Compare commits

..
Author SHA1 Message Date
Marc Billow 7c68d478af Bump version to 0.13.0 2026-07-27 14:54:26 +00:00
Marc Billow 3c15ca810b Merge pull request #132 from mbillow/claude/triage-issues-111-127-on5l50
Triage issues #111-#131: AC/oven/air-purifier detection fixes, new vacuum-station type, reconnect log noise
2026-07-27 09:46:11 -05:00
Marc Billow e9eb38740d Deduplicate ISO-timestamp parsing and document the new device type (review follow-up, issue #131)
- vacuum_station._parse_iso_utc was a verbatim copy of
  water_purifier._parse_iso_utc; promoted to common.parse_iso_utc and
  pointed both families at it. Also made it tzinfo-aware rather than
  unconditionally overwriting with UTC -- harmless today since every
  dump seen is a bare or Z-suffixed UTC timestamp, but a board that
  ever emits a real offset would otherwise have it silently clobbered.
- Added the new vacuum_station type to the README's supported-appliance
  table, and noted that combi microwaves route through the oven
  registry.
2026-07-27 14:43:02 +00:00
Marc Billow f37c9ec4ef Fix air-purifier fan power write and silent preset-mode rejection (review follow-up, issue #130)
- The fan's power write hardcoded /power/vs/0 while is_on already read
  /power/0 as a fallback -- a board reporting only the OCF resource
  would show correct state but silently no-op on every turn-on/off.
  Mirrors LocalThingsRangeHoodFan's existing _power_payload pattern:
  target whichever power href the board actually reports.
- async_set_preset_mode fell off the loop silently on an unmatched
  mode with no log and no error, unlike the rest of this codebase's
  write-rejection handling. Logs a warning now.
- Deduplicated HEPA_FILTER's usage-percent calculation, which was an
  inline reimplementation of airconditioner._filter_usage_percent;
  promoted the shared logic to common.filter_usage_percent and pointed
  both families at it.
- Gave air_purifier.SOUND_MODE its own translation_key instead of
  defaulting to the same catalog entry laundry.SOUND_MODE uses. That
  entry's state table is {voice, tone, mute}; this board's is
  {mute, buzzer} -- sharing it left 'buzzer' with no label.
2026-07-27 14:42:34 +00:00
Marc Billow d26e7b957b Fix unreachable reconnect-warning threshold (review follow-up, issue #119)
The 60s/5-reconnect window from the original fix could never actually
fire: consecutive reconnect attempts are never closer together than one
summary poll interval (30s) plus the 5s reconnect pause, so at most ~2
timestamps can ever land inside a 60s window regardless of how unhealthy
the connection is. That silently downgraded every reconnect to INFO
permanently, including the persistently-broken case the change was
supposed to still surface at WARNING.

Widen to a 300s window with a threshold of 3, which is reachable under
sustained failures and still a reasonable proxy for the README's
"actually broken" case.
2026-07-27 14:41:43 +00:00
Marc Billow b31f4f0c40 Merge pull request #128 from pedroperosin/fix/climate-set-temperature-hvac-mode
Honour hvac_mode in the AC climate set_temperature
2026-07-27 09:30:19 -05:00
Marc Billow 8b44fd5710 Add device support for the stick-vacuum clean/auto-empty station (issue #131)
A-VSKR-TP1-22-VS9500AL connects successfully but its dump shows no
vacuum-body state at all -- no suction level, no battery, no cleaning
mode -- only the clean/auto-empty station's own dustbag, dustbin
auto-empty settings, and UV-C sanitizing-cycle status. This strongly
suggests the WiFi/DTLS module lives in the station, not the handheld
stick, so the station is the only "device" this integration's local
API can reach at all.

New vacuum_station device type (these hrefs share nothing with any
existing family, so there's no shared-href ambiguity to resolve
against another type) routed via a new '-VSKR-' modelNum fallback.
Binds with zero unbound hrefs: dust-bag full/usage sensors, auto-empty
and dustbin auto-close switches, a discharging-time select, and
clean-station status including UV-C intensive mode, operation time,
and finished/emitted timestamps. A couple of fields with unconfirmed
exact semantics (stick_status, dustbag_usage's unit) are exposed as
plain diagnostic values rather than an asserted binary/percentage
meaning.
2026-07-27 14:11:06 +00:00
Marc Billow 4e1eb7d03d Add fan-mode control for TP1X_DA-AC-AIR air purifiers (issue #130)
This newer board family reports fan modes (Smart/Max/Mid/WindFree/Sleep)
directly on /mode/vs/0's top-level modes/supportedModes fields, unlike
the older ARTIK051_TVTL family this registry already supported, which
packs everything into an options[] array with no usable fan-speed
selector at all (see the module docstring's Comode_Off finding). Both
board generations share the /mode/vs/0 href, so the existing MODE
capability and a new FAN capability are discriminated by a match_fn
checking for the top-level supportedModes field, rather than adding a
new device type.

The fan entity only exposes PRESET_MODE, not an ordered percentage --
WindFree/Smart/Sleep are named behaviors, not "faster/slower" positions
relative to Max/Mid, matching how the AC family's own named convenient
modes are modeled as a preset rather than a speed number.

Also picked up the rest of this board's previously-unbound hrefs while
in there (display, HEPA filter, panel status, pet-filter mode, sound
settings), reusing airconditioner.DISPLAY_LIGHT and
airconditioner.MUTE_ONCE for the two hrefs identical to the shared
DA-AC- board family, since the "incomplete capability coverage" repair
was firing on more than just the fan gap the issue described.
2026-07-27 14:08:32 +00:00
Marc Billow e44fef7085 Route combi microwaves onto the existing oven registry (issue #121)
TP1X_DA-KS-MICROWAVE-01041 (MW7300B) reports no oneUiVersion and an
unrecognized consumer token, so it fell back to 'unknown' and only got
common capabilities. It shares the same '/oven/vs/0' cavity resource
and '/mode/vs/0' cook-mode shape (Convection/AirFryer/Grill/MicroWave*)
as the wall oven already supported, so this reuses that registry via a
new '-MICROWAVE-' modelNum fallback rather than adding a new device
type. The only href it didn't already cover was /recipe/cook/vs/0, an
empty quick-recipe-display blob bound with no entity per the 'don't
guess' rule.
2026-07-27 13:41:36 +00:00
Marc Billow 6f10a3b796 Support 2-axis wind oscillation and overload-protection status on newer WindFree boards (issue #126)
Newer TP1X_DA-AC-RAC-01011_0000 firmware (Bespoke AI WindFree Deluxe,
AR60H10D1JWNME) drops /wind/direction/vs/0 entirely and reports swing
via a separate vertical/horizontal Swing|Fix pair on
/wind/oscillation/vs/0 instead, which left swing_mode/swing_modes
silently empty and the href unbound. climate.py now falls back to the
oscillation resource when /wind/direction/vs/0 is absent, mapping the
same off/vertical/horizontal/both vocabulary the existing swing control
already uses.

Also binds the board's /anomalyload/vs/0 overload-response resource as
read-only diagnostic sensors (operation state + mode) -- the same
"don't guess" precedent as the existing CURRENT_LIMIT capability, since
nothing in the dump confirms the exact behavioral difference between
its 'Alarm' and 'PowerSaving' modes or whether toggling it is safe on
live HVAC hardware.

Most of the other gaps this issue reported (Fan-only mode, WindFree
preset labels, target-temperature channel selection, power on/off via
the vendor resource) turned out to already be fixed by the just-merged
cool-only global RAC work.
2026-07-27 13:41:04 +00:00
Marc Billow 2c8a765036 Downgrade a lone poll-failure reconnect to info (issue #119)
Samsung's firmware occasionally drops the DTLS session briefly --
normal appliance-side behavior per the README's "Known device
behavior" section -- so the coordinator recovering from that on its
own doesn't need a WARNING. Only escalate once reconnects pile up
within a trailing 60s window (5+), matching the README's own "more
than a handful per minute" definition of an actually broken
connection.
2026-07-27 13:40:32 +00:00
Marc Billow f62b4aeec8 Detect ARA-WW-TP1-22-COMMON wall-mount RACs as airconditioner (issues #115, #116, #117, #120)
AR10/13/18BYEAAWKNME report no oneUiVersion and no '_RAC_'/'-RAC-'
token at all, so for_device_by_model() fell through to 'unknown' and
every href went uncovered. The board carries the same TP1X-class
resource surface as every other room AC already supported (mode/
convenient/wind/temperature/power/filter/humidity), so this reuses the
existing airconditioner registry via a new 'ARA-WW-' modelNum fallback
rather than adding a new device type -- confirmed against all four
reporters' dumps binding cleanly with zero unbound hrefs.
2026-07-27 13:40:12 +00:00
Marc Billow 46dd91ce32 Add regression coverage for NE8300D range (issue #112)
TP1X_DA-KS-RANGE-0102X already resolves via the '-RANGE-' modelNum
fallback and /cooktopmonitoring/vs/0 already binds through
range.COOKTOP_MONITORING, so this model binds with zero unbound hrefs
today -- add a fixture to lock that in.
2026-07-27 13:39:51 +00:00
Marc Billow 085e80c92c Add regression coverage for WA55A7700AV washer (issue #111)
The DA_WM_TP1_21_COMMON board family already routes through the 'WA'
consumer-model-prefix fallback and binds cleanly against the washer
registry with zero unbound hrefs, but no fixture locked that in for
this specific board generation -- add one.
2026-07-27 13:39:30 +00:00
Pedro a9f21982e7 Honour hvac_mode in the AC climate set_temperature
HA's set_temperature service forwards an optional hvac_mode to the entity, and
the entity is expected to apply it. The AC climate entity ignored it and only
wrote the setpoint, so a set_temperature call carrying hvac_mode (e.g. a
dashboard "turn on to Auto 24" button) set the temperature but never changed the
mode or powered the unit on. Apply the mode first -- which powers the unit on
when it was off -- then the setpoint.
2026-07-27 09:57:31 -03:00
Marc Billow ef23f5ec32 Merge pull request #114 from mbillow/claude/pr91-finish
Add cool-only global RAC support: WindFree, Auto mode, and display light
2026-07-27 00:35:48 -05:00
Marc Billow 5547c401ed Address independent review findings on the RAC finish-up branch
- Pin the FAN_ONLY reverse-write fallback to 'Wind' (the original single
  spelling) instead of letting it silently flip to 'Fan' just because
  'Fan' was added second to the dict -- _device_code_for_hvac() resolves
  the code from a unit's own supportedModes first, so this dict is only
  a fallback for a unit reporting none at all, and that fallback
  shouldn't change behavior as an unintended side effect of insertion
  order.
- Add direct tests for _preset_to_ha() (pure function, previously
  untested) and the FAN_ONLY fallback pin.
- Cross-reference the AC family's inverted Light_On/Light_Off polarity
  against air_purifier.py's plain-polarity use of the same token name on
  the same resource name, so a future refactor doesn't assume they're
  the same thing.
- Fix a one-column continuation-line misalignment and a stale PR-number
  reference in a comment.
2026-07-27 05:11:58 +00:00
Marc Billow fa8f1b8675 Add cool-only global RAC support: WindFree, Auto mode, display light
Finishes PR #91's contribution (pedroperosin) with the requested review
changes applied, on our own branch:

- Preset resolution is now fully dynamic, read from each unit's own
  /mode/convenient/vs/0 supportedModes instead of a static per-model
  table -- any board's convenient modes (including WindFree's Nano/
  NanoSleep) surface without code changes. Unlabelled codes across
  existing fixtures (longwind, motionindirect, motiondirect, drycomfort)
  plus the two new WindFree ones are added to en.json/nl.json.
- 'Auto' now maps to HVACMode.AUTO instead of HEAT_COOL: these are
  single-setpoint "device decides" units, not two-setpoint heat+cool
  ones. 'Fan' is added alongside 'Wind' as a second FAN_ONLY spelling;
  a new _device_code_for_hvac() picks the code from the unit's own
  supportedModes since the flat map can't disambiguate two device codes
  mapping to one HA value.
- Detection gains a hyphenated '-RAC-' modelNum fallback (alongside the
  existing '_RAC_') for cool-only global RAC variants whose
  /otninformation/vs/0 ships no swVersionInfo block.
- Adds a display_light switch sourced from /mode/vs/0's opaque options
  blob (inverted Light_On/Light_Off token) for boards with no dedicated
  /light/vs/0 switch.

Write-path behavioural change (touches the path iterated on across
#9/#17/#27/#38/#54): power now targets the vendor /power/vs/0 instead of
the OCF /power/0, and _is_on() reads vendor-first. /power/0 is absent on
several known AC boards, so the previous OCF-first read/write pair could
report and act on stale state -- consistent with #53's "can turn on but
not off". Target temperature now picks its channel (OCF pair vs. vendor
items[]) based on which the unit actually reports, reading and writing
the same one.

The vendor temperature write and the new display-light write both carry
only the changed field(s), not the whole resource -- confirmed sufficient
on the wire, the device merges the rest itself. That requires the
coordinator's optimistic cache to do the same merge on the read side so a
setpoint change doesn't blank out current/min/max/unit for the settle
window: common.py gains merge_items_field() next to the existing
merge_options_field(), and async_send_command wires it in for any write
touching x.com.samsung.da.items.

New fixture/golden for the cool-only global RAC variant (TP1X_DA-AC-RAC-
01001, AI_RAC_GLOBAL_COOLONLY_3.0); display_light added to the goldens
for boards that gain the new options-based switch. Regenerated against
current main rather than copied from the original PR, since those
goldens had already shifted (current_temperature_c/humidity from issue
#75).
2026-07-27 05:00:06 +00:00
Marc Billow af00c2b938 Merge pull request #110 from mbillow/claude/device-support-issue-107-coffee
Add coffee-maker capabilities to the water purifier registry (issue #107)
2026-07-26 23:40:03 -05:00
Marc Billow 1177f0ed64 Merge pull request #109 from mbillow/claude/device-support-issue-106-wa-washer
Add WA (top-load washer) consumer-model prefix (issue #106)
2026-07-26 23:38:19 -05:00
Marc Billow 35775c3d4a Add coffee-maker capabilities to the water purifier registry (issue #107)
Some TP2X_WATERPURIFIER_20K units are coffee-capable and expose five
resources issue #90's original dump never had:
/favorite/coffee/vs/0, /favorite/hotwater/vs/0,
/brand/recipe/info/vs/0, /coffee/custom/recipe/vs/0,
/recipe/coffee/vs/0, and /recipe/coffee/deletion/vs/0.

Adds FAVORITE_HOTWATER (a switch + select pair on
/favorite/hotwater/vs/0, mirroring the existing FAVORITE_CAPACITY
pattern) and COFFEE (a switch + status sensor on
/favorite/coffee/vs/0). The remaining four hrefs are static
capability-advertisement blobs or empty on every dump seen so far --
no live "current recipe" or "current custom slot" field to expose --
so they're added to the ignored coverage list per the 'don't guess'
rule rather than modeled speculatively.

Confirmed against the issue #107 diagnostics dump with zero unbound
hrefs.
2026-07-27 04:19:19 +00:00
Marc Billow 8ac1d11d17 Add WA (top-load washer) consumer-model prefix (issue #106)
WA8000T reports no oneUiVersion and used the 'WA' consumer-model
prefix, unmapped in _CONSUMER_PREFIX_TO_KEY (only WW/WD/WF/WV were
covered), so it fell into the unknown-device-type fallback.

Adding a bare 'WA' entry collided with the unrelated '_WAC_' (Window
Air Conditioner, issue #87) board-family token: some devices report
description == modelNum, so 'WAC' shows up as its own description
segment and 'WAC'[:2] == 'WA' matched the new washer prefix before the
more specific '_WAC_' modelNum check ever ran. Fixed by reordering
for_device_by_model() to check board-family modelNum tokens first and
the fuzzier 2-letter consumer-model-prefix scan only as a fallback,
rather than patching the prefix matching itself (an earlier attempt --
requiring a digit immediately after the prefix -- broke issue #79's
real DVE50A8800 case, which has no digit there either). Added a
regression test pinning the WAC/WA disambiguation directly.

Confirmed against the issue #106 diagnostics dump with zero unbound
hrefs.
2026-07-27 04:15:08 +00:00
Marc Billow b3a9192088 Merge pull request #98 from mbillow/claude/device-support-issue-86-cooktop
Add standalone induction-cooktop support (issue #86)
2026-07-26 23:04:51 -05:00
Marc Billow 634f367e96 Merge remote-tracking branch 'origin/main' into claude/device-support-issue-86-cooktop
# Conflicts:
#	custom_components/localthings/registry/by_type/__init__.py
2026-07-27 04:04:19 +00:00
Marc Billow 482e238a7a Merge pull request #102 from mbillow/claude/device-support-issue-77-freezer
Device support issue #77: freezer
2026-07-26 23:03:59 -05:00
Marc Billow f31cee206b Merge remote-tracking branch 'origin/main' into claude/device-support-issue-77-freezer
# Conflicts:
#	custom_components/localthings/registry/by_type/__init__.py
#	tests/test_by_type.py
2026-07-27 04:03:38 +00:00
Marc Billow 14b5a7cb68 Merge pull request #96 from mbillow/claude/device-support-issue-88-dehumidifier
Add device support for Samsung dehumidifiers (issue #88)
2026-07-26 23:00:33 -05:00
Marc Billow 8628ae6864 Merge remote-tracking branch 'origin/main' into claude/device-support-issue-86-cooktop
# Conflicts:
#	custom_components/localthings/registry/by_type/__init__.py
#	custom_components/localthings/translations/en.json
#	custom_components/localthings/translations/nl.json
2026-07-27 04:00:15 +00:00
Marc Billow 38263eaa72 Merge remote-tracking branch 'origin/main' into claude/device-support-issue-88-dehumidifier
# Conflicts:
#	custom_components/localthings/registry/by_type/__init__.py
#	tests/test_by_type.py
#	tests/test_golden_regression.py
2026-07-27 03:58:53 +00:00
Marc Billow ff6e1ecff4 Rename gas cooktop registry's display name to avoid induction_cooktop confusion
Renames DeviceRegistry.name from 'cooktop' to 'gas_cooktop' for the
NA9300K-class gas-cooktop registry (PR #23), so diagnostics/device-info
labels no longer collide with the unrelated induction_cooktop family
(issue #86) -- two different OCF surfaces that happen to share the
English word "cooktop".

Safe rename: _REGISTRY_BY_KEY's 'cooktop' lookup key is unchanged, so
all three existing detection paths (oneUiVersion "Cooktop" exact
match, the legacy ARTIK051 modelNum rule, and the resource-signature
fallback) keep routing real devices exactly as before. Entity
unique_ids are built from device serial + entity key, not registry
name, so existing entities are unaffected. Only the DeviceInfo.name
and diagnostics device_type strings change, both cosmetic.
2026-07-27 03:56:32 +00:00
Marc Billow 9ccaa3f01d Merge pull request #95 from mbillow/claude/device-support-issue-90-water-purifier
Add device support for Samsung water purifiers (issue #90)
2026-07-26 22:53:40 -05:00
Marc Billow 78f1bb0f92 Fix unclosed PROBE_STATUS capability from a bad merge conflict resolution
The merge of main into this branch dropped PROBE_STATUS's closing
),\n) and glued issue #74's COOKTOP_MONITORING addition directly onto
its entities tuple, leaving an unclosed paren (SyntaxError on import,
breaking Pytest and Hassfest CI). Restores the closing parens; no
functional change.
2026-07-27 03:46:55 +00:00
Marc Billow 5e60f9b953 Fix stale Window AC golden fixture (current_temperature_c, humidity)
PR #75 (WindFree AC) added CURRENT_TEMPERATURE/HUMIDITY capabilities
to the shared airconditioner registry, which every AC device picks up
-- including the Window AC from PR #87. Both branches built their
golden fixtures independently against their own base commit before
either landed, so neither saw the other's addition; once both merged,
the Window AC's fixture went stale. The two extra keys are real,
working sensors from #75's work, not a regression.
2026-07-27 03:45:32 +00:00
Marc Billow 3b0f4f8b66 Merge branch 'main' into claude/device-support-issue-90-water-purifier 2026-07-26 22:06:20 -05:00
Marc Billow 2aaaad71a8 Merge branch 'main' into claude/device-support-issue-88-dehumidifier 2026-07-26 22:05:24 -05:00
Marc Billow 4719e3aa80 Merge pull request #97 from mbillow/claude/device-support-issue-87-window-ac
Add device support for Bespoke Window AC (issue #87)
2026-07-26 22:03:36 -05:00
Marc Billow 84d73105cf Merge branch 'main' into claude/device-support-issue-86-cooktop 2026-07-26 21:58:21 -05:00
Marc Billow d0e03b026c Merge pull request #99 from mbillow/claude/device-support-issue-83-fridge-detection
Catch the 'Nothing(SVC)' placeholder serial in both fallback sites (i…
2026-07-26 21:55:33 -05:00
Marc Billow d2f0a5c976 Merge pull request #100 from mbillow/claude/device-support-issue-80-cycle-labels
Add confirmed washer/dryer cycle code labels (issue #80)
2026-07-26 21:53:21 -05:00
Marc Billow 48b34c9e31 Delete test_issue_80_cycle_codes_are_labelled
Removed test for issue 80 regarding cycle codes.
2026-07-26 21:53:06 -05:00
Marc Billow 0b6eda0cdc Merge branch 'main' into claude/device-support-issue-80-cycle-labels 2026-07-26 21:51:23 -05:00
Marc Billow 285d2de397 Merge pull request #101 from mbillow/claude/device-support-issue-79-dryer
Fix dryer detection when description pairs two model numbers (issue #79)
2026-07-26 21:45:12 -05:00
Marc Billow 8b45d8cbf6 Merge branch 'main' into claude/device-support-issue-77-freezer 2026-07-26 21:40:51 -05:00
Marc Billow f3b0ef4733 Merge pull request #103 from mbillow/claude/device-support-issue-75-windfree-ac
Add humidity/temperature sensors and horizontal swing for AC (issue #75)
2026-07-26 21:37:59 -05:00
Marc Billow d20d4e2158 Merge pull request #104 from mbillow/claude/device-support-issue-74-range
Add range/oven detection for boards missing /information/vs/0 (issue …
2026-07-26 21:33:25 -05:00
Marc Billow d321fe72cd Merge pull request #94 from mbillow/claude/device-support-issue-93-ac-modes
Device support issue 93: ac modes
2026-07-26 21:30:46 -05:00
Marc Billow 9b09f1aee3 Merge pull request #70 from vmvarga/feat/wd7000b-fridge-fixes
feat: laundry firmware-flag gating, fridge vendor temp writes, course…
2026-07-26 21:25:26 -05:00
Marc Billow 45931332d4 Rework AIComfort as an HVACMode.AUTO + preset overlay, add unmapped-mode warning (issue #93)
AIComfort isn't a distinct thermodynamic operation like Cool/Dry/Heat --
it's an AI-driven overlay on top of the device's own 'Auto' behavior,
confirmed by A-CAWW-TP2-20-COMMON reporting both 'Auto' and 'AIComfort'
as separate, mutually-exclusive entries in /mode/vs/0's supportedModes.
Modeled the idiomatic HA way instead of a flat _DEVICE_TO_HVAC entry:
hvac_mode reports AUTO and a new 'ai_comfort' preset carries the
distinction. Entered/left only via the preset (writes the primary mode
resource, not the convenient one) -- there's no dedicated HVACMode
value for it, so it's not offered in the hvac_mode dropdown directly.

Also adds a once-per-(href, code) warning log when a device-reported
mode has no entry in the relevant map, so a future gap like this one
surfaces in the log instead of silently vanishing -- the exact failure
mode issue #93 called out ("this class of gap is invisible without
diffing against supportedModes").
2026-07-27 02:01:51 +00:00
Marc Billow 8c3250b163 Map AC's AIComfort mode to HVACMode.AUTO (issue #93)
A-CAWW-TP2-20-COMMON (and likely other CAWW/TP2X-class boards) reports
'AIComfort' as a distinct entry in /mode/vs/0's supportedModes,
alongside 'Auto' (already mapped to HEAT_COOL). _read_modes() silently
drops any code missing from _DEVICE_TO_HVAC, so AIComfort was
unreachable -- one of HA's five other AC hvac_modes, unused by this
device family until now.

The other two gaps in issue #93 are already addressed elsewhere and
not duplicated here:
- Fan-only via the 'Fan' device code, and the WindFree/LongWind/
  NanoSleep preset codes, are covered by the still-open PR #91, which
  replaces the static preset table with a fully dynamic resolver over
  the device's own supportedModes.
- The 'Left_And_Right' -> horizontal swing mapping is already on the
  still-open claude/device-support-issue-75-windfree-ac branch.
2026-07-27 01:43:22 +00:00
Marc Billow d53459d047 Add device support for Samsung water purifiers (issue #90)
The TP2X_WATERPURIFIER_20K water purifier reports no oneUiVersion and
its modelNum/description don't match any consumer-prefix or existing
board-family token, so it fell into the unknown-device-type fallback
with only common capabilities. Add a new water_purifier registry,
routed via a 'WATERPURIFIER' modelNum/description fallback rule.

Models dispense settings (type/temperature/capacity/pouring status),
sterilize and filter status, favorite-capacity presets, and the three
water/buzzer locks. Per the adding-device-support skill's "never
hard-code the one dump's values" rule: dispense-capacity bounds and
step come live from the device's own desiredCapacityRange/
capacityResolution fields (range_field/step_fn), not a hardcoded
constant, and the hot-water-temperature control is a select over the
live supportedHotTemperatures list rather than a number with invented
bounds, since only a few discrete temperatures are selectable.

/mode/vs/0 and /automation/waterpurifier/vs/0 are left unmodeled: the
former carries an opaque wizard-workflow token with no coherent
current-value contract, the latter is a static support-flags blob with
no live setting to expose.

Confirmed against the issue #90 diagnostics dump with zero unbound
hrefs. Updates the README's supported-appliance-types table for the
new device type.
2026-07-27 01:40:05 +00:00
Marc Billow 89429039fb Add device support for Samsung dehumidifiers (issue #88)
The AY18CG7500GED dehumidifier (modelNum TP1X_DA_AC_DHM_01001_0000)
shares the DA_AC_ board family with the room-AC models but carries the
'_DHM_' token instead of '_RAC_'/'_PRAC_'/'_WAC_', so it fell into the
unknown-device-type fallback. Add a new dehumidifier registry, routed
via a '_DHM_' modelNum fallback rule, distinct from airconditioner
since target humidity (not temperature) is the primary control and
there's no climate composite.

Reuses airconditioner.py's AUTO_CLEAN/AIR_FILTER/MUTE_ONCE capabilities
directly (identical resource shapes on the shared board family). Adds
a new humidity sensor + target-humidity number pair and an
operating-mode select. Per the adding-device-support skill's
"never hard-code the one dump's values" rule, the target-humidity
number has no hardcoded min/max (falls back to HA's own 0-100 default
for a percentage field) and reads its step live from the device's own
`increment` field rather than a spec-sheet-derived constant. The
operating-mode select's options come live from supportedModes.

/mode/convenient/vs/0 is left unmodeled: only supportedModes is present
on this dump, with no live current-value field to confirm a read/write
contract.

Confirmed against the issue #88 diagnostics dump with zero unbound
hrefs. Updates the README's supported-appliance-types table for the
new device type.
2026-07-27 01:29:25 +00:00
Marc Billow b47eeaf7bc Add device support for Bespoke Window AC (issue #87)
The AW06C7155EWAZ window air conditioner (modelNum
TP1X_DA_AC_WAC_01001_0000) reports no oneUiVersion and uses the '_WAC_'
(Window Air Conditioner) modelNum token instead of the '_RAC_'/'_PRAC_'
tokens already handled by for_device_by_model. Add a fallback rule for
that token, routing it to the existing airconditioner registry.

The device's resource surface (mode/convenient/wind/temperature/power/
filter/humidity) is already fully modeled by the airconditioner
capability set, so this is purely a detection-routing fix -- confirmed
against the issue #87 diagnostics dump with zero unbound hrefs.
2026-07-27 01:17:45 +00:00
Marc Billow a453dc9ece Add standalone induction-cooktop support (issue #86)
TP1X_DA-KS-COOKTOP-01011 (NV8500T-/KO4) is the same board family and
/cooktop/status/vs/0 resource shape as issue #44's range combo, minus
the oven -- but its modelNum uses the hyphenated '-COOKTOP-' token,
which for_device_by_model's existing '_COOKTOP' check (underscore-
delimited, matching the unrelated NA9300K gas-cooktop family in
cooktop.py) doesn't match. The device fell back to 'unknown' with only
energy/alarms from the global fallback.

Add the hyphenated token check, routing to a new 'induction_cooktop'
registry (by_type/induction_cooktop.py) that reuses range.py's
COOKTOP_STATUS/COOKTOP_SPEC/COOKTOP_SAFETY/PROBE_STATUS and
cooktop.PAIRED_HOOD_STATUS wholesale rather than pulling in range.py's
oven capabilities, which this device has no hrefs for at all.

Along the way, three fields the reporter asked for turned out to be
gaps in the shared range.py capability itself, not just missing
routing -- also present (and previously unmodeled) on issue #44's
original combo-range dump:
- /cooktop/status/vs/0's own `power` and `childLock` fields (distinct
  from common.POWER's /power/0 or /power/vs/0, which a combo range
  additionally carries for the whole appliance) -- childLock gets a
  write_fn (a safe lock toggle, direct single-field PUT), power stays
  read-only (no live device to confirm a remote write wouldn't leave a
  burner active unattended).
- Each burner's `panDetection` field.

New: a Bluetooth meat-probe capability for /bluetooth/probe/status/vs/0
(read-only -- connection, battery, current/target temperature), and
/cooktop/recipe/status/vs/0 is ignored (every field empty on this idle
dump, same treatment as the microwave family's /recipe/cook/vs/0).

Regenerates the range golden fixture (gains cooktop_power/
cooktop_child_lock/burner_N_pan_detected) and adds a dedicated fixture
for the standalone cooktop.
2026-07-27 01:12:16 +00:00
Marc Billow 2c9fda3db1 Catch the 'Nothing(SVC)' placeholder serial in both fallback sites (issue #83)
The ARTIK051_DONGLE_REF firmware family reports the literal string
"Nothing(SVC)" as serialNum on every unit -- non-empty, so the existing
`if not serial` checks in config_flow.py's _probe_and_validate and
coordinator.py's _run_discovery don't catch it. Two such appliances on
one install (a fridge and a freezer, each its own dongle) then collide:
config_flow gives both the same unique_id and rejects the second as
"already configured" (bug 2), and even once that's worked around,
device_serial feeds every entity's unique_id too, so the second
appliance's entities get silently dropped with "does not generate
unique IDs" log lines (bug 4).

Add _is_placeholder_serial (duplicated in both modules rather than
imported, to avoid pulling config_flow into the runtime coordinator's
import graph or vice versa for a two-line check) and treat it the same
as an empty serial: fall back to host/port.

Type detection (bug 1) and the door sensor's field-name gap (bug 3),
also reported in this issue, are already fixed via the
claude/device-support-issue-77-freezer branch, which hit the same
ARTIK051_DONGLE_REF family from a different report -- not duplicated
here.
2026-07-27 01:00:05 +00:00
Marc Billow 5d7789eb4e Add confirmed washer/dryer cycle code labels (issue #80)
Five raw course codes were rendering unlabeled because no translation
entry existed for them, confirmed by the reporter selecting each cycle
on the physical appliance and reading back the raw code from the
entity's state:

- washer_cycle_table_02: '52' Eco Cold, '54' Towels, '60' Self Clean+
  (a WF50A8600AV/US). '54' shares a display name with the existing '24'
  Towels -- a different code on the same table legitimately landing on
  the same label, matching the existing '21'/'65' Colors and
  '27'/'5E' Rinse+Spin pairs, not a duplicate-in-error.
- dryer_cycle_table_03: '01' Normal, '06' Time dry (a DVE50A8600V/A3,
  the same model added in the previous commit's detection fix).

No code changes -- select.py already derives which raw values it
normalizes from the shipped catalog, so labelling a code is purely a
translations/en.json (mirrored to nl.json) addition.
2026-07-27 00:47:16 +00:00
Marc Billow bae1ac4337 Fix dryer detection when description pairs two model numbers (issue #79)
DVE50A8600V/A3 reports description
'DA_WM_TP1_21_COMMON_DVE50A8800_8600/DC92-02835A_0080' -- a paired
listing of two related model numbers (DVE50A8800 and DVE50A8600) joined
by an underscore, rather than the usual single trailing consumer-model
token. for_device_by_model only ever checked the literal last
underscore segment ('8600', which has no recognizable 2-letter prefix
on its own), so the real 'DV' token one segment earlier was never
reached and the device fell back to 'unknown' with only common
capabilities -- no dry level, cycle, or wrinkle-prevent entities.

Replace the single last-segment extraction with _consumer_model_key,
which scans segments from the end and returns the first one that
resolves. Behavior is unchanged for every existing single-token
description (the last segment still matches first); it just keeps
looking when that segment doesn't.
2026-07-27 00:44:23 +00:00
Marc Billow 643aac92f8 Lock in the same ARTIK051_DONGLE_REF fix for the fridge half (issue #78)
RR40M7165WW is the fridge half of the same household dongle setup as
issue #77's freezer -- identical pipe-delimited ARTIK051_DONGLE_REF
modelNum, so the previous commit's detection and door-sensor fixes
already cover it with no further code changes. Add its fixture as a
second, independent regression case: it exercises the 'cooler' instance
segment instead of 'freezer' for both the temperature pattern caps and
DOOR_GENERIC, and notably reports /door/onedoorfreezer/vs/0 despite
being a single-door fridge (shared firmware naming across the product
line, not an actual second compartment) -- worth having its own golden
so that stays working too.
2026-07-27 00:41:25 +00:00
Marc Billow 8194ea9e87 Fix ARTIK051_DONGLE_REF freezer type detection and door sensor (issue #77)
RZ32M713EWW/EE (an ARTIK051-dongle standalone freezer) reports no
oneUiVersion and a pipe-delimited modelNum
('ARTIK051_DONGLE_REF|<rest>') -- REF is the last underscore segment
before the pipe, not wrapped in underscores on both sides like the
'..._REF_...' shape for_device_by_model's substring check expected, so
the device fell through to 'unknown' with only common capabilities:
no door sensor, no temperature sensors, nothing fridge-specific.

Replace the substring check with a segment-based one
(_model_num_segments splits the pipe-delimited prefix on '_') that
catches both shapes. Same root cause, same fix, and same device family
independently reported and root-caused in issue #83 (which also covers
two further bugs -- config-flow/coordinator serial collisions on a
second symptom of this firmware, 'Nothing(SVC)' as a literal serial --
not needed here since this reporter has a single unit; left for #83).

Once routed to the refrigerator registry, fridge.py's existing pattern
capabilities pick up /temperature/current/freezer/0 and
/temperature/desired/freezer/0 for free -- they were never the actual
problem, just unreachable under the 'unknown' fallback (which never
tries pattern capabilities at all). The door sensor needed one more
fix: /door/onedoorfreezer/vs/0 reports the vendor-prefixed
x.com.samsung.da.openState, not the bare openState DOOR_GENERIC read,
so the entity existed but stayed permanently unavailable. Check both
field names via rep_fn.
2026-07-27 00:39:33 +00:00
Marc Billow 3afbe6c6c1 Add humidity/temperature sensors and horizontal swing for AC (issue #75)
Three gaps reported against an ARTIK051_PRAC_20K WindFree unit vs. the
SmartThings integration:

1. Missing WindFree/motion convenient-mode presets -- left alone here.
   PR #91 replaces climate.py's static _DEVICE_TO_PRESET table with a
   generic resolver that reads any preset code straight off the unit's
   own supportedModes, which already covers this (and more generically
   than a per-model dict would) -- adding one here would just conflict.

2. No horizontal oscillation: /wind/direction/vs/0's supportedModes
   includes Left_And_Right, which _DEVICE_TO_SWING had no mapping for.
   Add it to HA's standard 'horizontal' swing constant.

3. No standalone humidity/current-temperature sensors: the climate card
   already reads both internally, but nothing exposed them as entities
   for history/automations. Add CURRENT_TEMPERATURE (OCF
   /temperature/current/0) with a CURRENT_TEMPERATURE_VS vendor fallback
   (same match_fn-gated pair shape as common.py's POWER_GENERIC/
   POWER_VS_FALLBACK), and a HUMIDITY sensor reading /humidity/vs/0's
   fivepercentHumidity field -- the only one of the three
   humidity-shaped fields across /humidity/0 and /humidity/vs/0 that
   isn't permanently stuck at 0 on every dump seen.

Regenerates the five existing AC goldens (all pick up
current_temperature_c; most pick up humidity) and adds a dedicated
fixture from the issue's WindFree dump, whose /humidity/vs/0 actually
has live fivepercentHumidity data.
2026-07-27 00:33:35 +00:00
Marc Billow c72b5a988e Add range/oven detection for boards missing /information/vs/0 (issue #74)
NE63B8411SS reports no oneUiVersion and no /information/vs/0 resource at
all, so neither for_device nor for_device_by_model's modelNum tokens have
anything to key off -- it fell through to the unknown-device fallback,
which also mis-binds /temperatures/vs/0 against fridge.py's setpoint
capability (a collision the oven family's own capabilities are normally
excluded from the global registry specifically to avoid).

Add a resources-based signature to for_device_by_resources(): 'Bake' in
/mode/vs/0's supportedModes is oven/range-exclusive vocabulary, and paired
with the /oven/vs/0 cavity resource it reliably identifies this family
even with no model info at all. Route to 'range' when a cooktop-status
resource is also present, else plain 'oven'.

This board's cooktop half also doesn't expose the per-burner
/cooktop/status/vs/0 array range.py already models -- only the coarser
/cooktopmonitoring/vs/0 summary resource. Add a read-only
COOKTOP_MONITORING capability for it (cooktop running state, warming
center state) rather than leaving it unbound.
2026-07-27 00:23:04 +00:00
vmvarga 2fd8ff8260 merge temp_setpoint 2026-07-26 11:05:13 +02:00
vmvarga 00cf8d676d feat: laundry firmware-flag gating, fridge vendor temp writes, course names 2026-07-25 10:01:56 +02:00
Marc Billow aff7647460 Merge pull request #65 from Metal-Eagle/feature/add-completion-time
Feature/add completion time
2026-07-24 23:46:47 -05:00
Jeroen Hof 705da781f1 Merge branch 'main' into feature/add-completion-time 2026-07-25 06:41:17 +02:00
Jeroen Hof 6de1a4090e i18n: simplify completion time translation in English and Dutch 2026-07-25 06:02:45 +02:00
Jeroen Hof ef75db6697 i18n: update completion time translation in English and Dutch 2026-07-25 05:55:47 +02:00
Jeroen Hof 9e290f27ee refactor: update sensor descriptions for completion time and delay start 2026-07-25 05:24:10 +02:00
Jeroen Hof 37bfff152d refactor: remove unused strings.json file 2026-07-25 05:15:34 +02:00
Jeroen Hof 317b7d0c1b fix: add missing newline at end of en.json file 2026-07-25 05:09:55 +02:00
Jeroen Hof ba8e41d310 Implement code changes to enhance functionality and improve performance 2026-07-25 05:08:18 +02:00
Jeroen Hof 724686c42c i18n: add translations for AI Wash and Jeans 2026-07-25 05:01:40 +02:00
Jeroen Hof edccb6ae3f i18n: add new wash options for AI Wash and Jeans 2026-07-25 04:59:37 +02:00
Jeroen Hof 4a91f62a36 i18n: add new translations for AI Wash and Spijkerbroek 2026-07-25 04:49:40 +02:00
Jeroen Hof 1d4d38db14 Merge branch 'main' into feature/add-completion-time 2026-07-25 04:47:22 +02:00
Marc Billow 35e2c79b14 brand: use updated localthing logo 2026-07-24 20:23:48 -05:00
Marc Billow f185951a8a i18n: missed a quick wash translation 2026-07-24 15:42:24 -05:00
Marc Billow adcb8a0ecb chore: add additional translations provided in #1 dutch 2026-07-24 15:33:31 -05:00
Marc Billow dc86c7f0ef Merge pull request #72 from mbillow/claude/home-assistant-i18n-iwwyvr
feat (i18n): refactor and improve translation support
2026-07-24 15:23:39 -05:00
Marc Billow 30d38d855c Merge branch 'main' into claude/home-assistant-i18n-iwwyvr 2026-07-24 15:23:10 -05:00
Marc Billow ee77e3bda8 chore: add additional translations provided in #1 2026-07-24 15:20:19 -05:00
Marc Billow 10c850f2b9 refactor(i18n): drop the vestigial descriptor name field
Since entity.py started routing named descriptors through the catalog
under desc.key, SamsungEntityDescription.name has been read for its
value nowhere -- only twice as a flag, to decide whether an entity was
translated at all. That left 148 English names duplicated between Python
and translations/en.json with nothing keeping them honest: six had
already drifted, invisibly, because editing the Python side changes
nothing a user sees.

So the field is gone, and translation_key defaults to desc.key. A
descriptor now sets translation_key only to share one catalog entry
across descriptors or to point at a differently-named one, and the
catalog is the only place an entity name exists.

Every descriptor resolves to exactly the translation key, icon, entity
category, enabled-default and gating it did before -- with one
deliberate exception: the hood fan, previously the sole descriptor with
no key at all, now resolves to 'fan'. That is inert, because fan.py sets
_attr_name = None so the entity presents as the device itself.

The three helpers that forwarded a name into a descriptor
(laundry.bool_option_switch, washer._bool_option_switch, air_purifier's
sensor table) lose that parameter. test_translations.py now requires a
catalog entry for every descriptor rather than only translated ones.

Claude-Session: https://claude.ai/code/session_01GiibJZZLWVvyxq7mc7EDNp
2026-07-24 19:45:37 +00:00
Marc Billow 6281b40549 refactor(i18n): make the shipped catalog the single source of truth
PR #68 restated its own translation data in Python: a 60-line
TRANSLATED_SELECT_STATES table of frozensets duplicating every
entity.select.*.state key, a second _TRANSLATED_COURSE_TABLES table
naming which course tables have translations, and a strings.json that
was a 835-line byte-for-byte copy of translations/en.json save 43
[%key:...%] references. Each needed hand-syncing, and one was already
drifting.

Home Assistant loads exactly one file per language for a custom
integration -- translations/<lang>.json. It never reads strings.json and
never resolves [%key:...%]; both belong to Core's build tooling, which
custom integrations don't run through (hassfest skips a missing
strings.json and validates translations/en.json instead). So en.json is
the source, and the new catalog.py reads the keys and states back out of
it for the two decisions Python genuinely has to make:

  - select._display() normalizes a raw Samsung option to a lowercase
    state key only when the catalog knows it, else leaves the vendor's
    casing alone. Derived sets are identical to the removed literals.
  - laundry.cycle_select() keys off a device-reported course table only
    when that table has an entry, else falls back to the name-only
    'cycle' key. Translating Table_00 is now a translations-only change.

Also fixes six names that had already drifted between the Python
descriptors and the catalog, restoring HA's sentence case for two
generic ones (Auto release dry, Bubble soak) and taking the catalog's
wording for the rest, and adds a test so the vestigial descriptor names
can't silently disagree with the UI again.

Claude-Session: https://claude.ai/code/session_01GiibJZZLWVvyxq7mc7EDNp
2026-07-24 19:32:09 +00:00
Marc Billow 0b6cc6aa9f Merge branch 'pr68' into claude/home-assistant-i18n-iwwyvr 2026-07-24 19:24:59 +00:00
Hmmbob 5d86dbe7e8 Resolve runtime translation references 2026-07-24 21:13:07 +02:00
Hmmbob 82acc05d38 Limit changes to translation support 2026-07-24 18:40:44 +02:00
Hmmbob da49825e2e Harden unknown vendor value fallbacks 2026-07-24 18:12:01 +02:00
Hmmbob b85af10ae1 Test translation coverage and dynamic fallbacks 2026-07-24 18:08:29 +02:00
Hmmbob f9ebc8e286 Make LocalThings UI fully translatable 2026-07-24 18:08:29 +02:00
Hmmbob d0cfaeebc1 Add Dutch translations 2026-07-24 17:29:50 +02:00
Jeroen Hof 365a1c4722 feat: enhance operational capabilities by adding completion_minutes and updating related logic; remove completion_time references 2026-07-24 15:35:17 +02:00
Jeroen Hof 7be91b56e8 Merge remote-tracking branch 'upstream/main' into feature/add-completion-time 2026-07-24 08:23:42 +02:00
Jeroen Hof 2c283218b0 feat: add completion_minutes and completion_time to state_keys in JSON fixtures 2026-07-24 08:10:17 +02:00
Jeroen Hof ab874797fc Merge branch 'main' into feature/add-completion-time 2026-07-24 07:48:57 +02:00
Jeroen Hof f5d4e2219a fix: remove redundant course codes from parse_edit_course_list test case 2026-07-24 07:47:53 +02:00
Jeroen Hof 86f07abf30 fix: correct expected output for parse_edit_course_list test case 2026-07-24 06:29:53 +02:00
Jeroen Hof b00390c093 fix: correct key for Jeans cycle in strings and translations 2026-07-24 06:27:28 +02:00
Jeroen Hof e913a3c2c0 feat: add new washer cycle options for AI Wash and Jeans 2026-07-24 06:26:24 +02:00
Jeroen Hof b2db4bf357 feat: add completion time and minutes sensors with parsing logic 2026-07-24 06:23:53 +02:00
125 changed files with 13501 additions and 971 deletions
+37 -9
View File
@@ -103,16 +103,44 @@ 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. Enum selects need translation support
## 5. Names and enum labels live in translations, never in Python
Any select whose options are raw device codes (course/cycle, and code-valued
settings) must render through translations, not Python:
- Set `translation_key='<family>_cycle'` (or similar) on the `SelectDesc`;
`options`/`options_field` supply the **raw** codes.
- Add the labels to **both** `strings.json` and `translations/en.json` under
`entity.select.<translation_key>.state.<code>`, with the code **lowercased**
(e.g. `"16": "Cotton"`). Codes with no entry render as the raw code — that's
the cue to identify and name them.
Descriptors have **no `name` field**. Every entity is named from the shipped
catalog, keyed by `translation_key` — which defaults to the descriptor's own
`key`. So adding `SensorDesc(key='filter_status', ...)` obliges you to add:
```json
"entity": { "sensor": { "filter_status": { "name": "Filter status" } } }
```
to `translations/en.json`. Skip it and the entity ships nameless;
`tests/test_translations.py` fails the build instead.
- **Sentence case** ("Filter status", not "Filter Status"), per HA's style
guide — capitalize only proper nouns and Samsung feature names ("AI Energy
Mode", "Storm Wash+").
- Set `translation_key` explicitly only to **share** one catalog entry across
descriptors, or to point at a differently-named one. Two descriptors on the
same platform with the same `key` already share an entry — intended for
`common.py`'s OCF/vendor fallback pairs, a silent mislabel otherwise.
- Prefer HA's own vocabulary where it fits: a `device_class` gives you
translated states for free (`binary_sensor` door/running, `sensor`
timestamp/enum), so don't restate them.
Selects whose options are raw device codes (course/cycle, code-valued
settings) additionally need those codes labelled:
- `options`/`options_field` supply the **raw** codes; the catalog maps them.
- Add labels under `entity.select.<translation_key>.state.<code>`, code
**lowercased** (e.g. `"16": "Cotton"`). `select.py` derives which values it
normalizes from the catalog itself, so there is no Python list to keep in
sync — a code with no entry simply renders as the raw code, which is the cue
to identify and name it.
`translations/en.json` is the only place any of this lives: there is no
`strings.json` (Home Assistant doesn't read one from a custom integration) and
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
+14 -3
View File
@@ -1,3 +1,8 @@
<!-- dark mode -->
<img src="custom_components/localthings/brand/dark_logo@2x.png#gh-dark-mode-only" alt="LocalThings Logo"/>
<!-- light mode -->
<img src="custom_components/localthings/brand/logo@2x.png#gh-light-mode-only" alt="LocalThings Logo"/>
# LocalThings
**A native Home Assistant custom integration for local control of newer-generation Samsung connected appliances.** No cloud round-trip. Add a device through HA's normal *Settings > Devices & Services* flow and it talks CoAP-over-DTLS straight to the appliance on your LAN.
@@ -18,14 +23,17 @@ Your state stays on your LAN: HA talks to the appliance over a direct DTLS sessi
|---|---|
| Air conditioner | `by_type/airconditioner.py` |
| Air purifier | `by_type/air_purifier.py` |
| Dehumidifier | `by_type/dehumidifier.py` |
| Dryer | `by_type/dryer.py` |
| Oven | `by_type/oven.py` |
| Cooktop (read-only burner status) | `by_type/cooktop.py` |
| Oven (including combi microwaves) | `by_type/oven.py` |
| Gas cooktop (read-only burner status) | `by_type/cooktop.py` |
| Range hood | `by_type/range_hood.py` |
| Range | `by_type/range.py` |
| Dishwasher | `by_type/dishwasher.py` |
| Refrigerator | `by_type/refrigerator.py` |
| Washer | `by_type/washer.py` |
| Water purifier | `by_type/water_purifier.py` |
| Vacuum clean/auto-empty station | `by_type/vacuum_station.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.
@@ -106,7 +114,10 @@ custom_components/localthings/
entity.py Base entity wiring capability registry -> HA entity
sensor.py / binary_sensor.py / switch.py / number.py / select.py / button.py / time.py / fan.py / climate.py
One module per HA platform
strings.json / translations/ Config-flow copy + entity state translations
catalog.py Reads the shipped translation catalog (which keys/states exist)
translations/ Config-flow copy + entity name/state translations, one file per
language; en.json is the source of truth (no strings.json —
Home Assistant never reads one from a custom integration)
registry/
registry.py Builds the global capability registry, validates href collisions
capability.py Capability dataclass (href, entities, transforms)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 146 KiB

+49
View File
@@ -0,0 +1,49 @@
"""The integration's own shipped translation catalog, as data.
Home Assistant loads exactly one file per language for a custom integration:
``translations/<lang>.json``. It does not read ``strings.json`` and it does
not resolve ``[%key:...%]`` references -- both of those belong to Core's
build tooling (``script/translations``), which custom integrations never run
through. So ``translations/en.json`` is not a generated artifact here, it is
the source of truth, and English is the one catalog required to be complete
(hassfest validates it for custom integrations).
That makes the English catalog the authority on *which* translation keys and
states exist, which is something the Python side genuinely needs to know:
* ``select`` has to decide whether to normalize a raw Samsung option into
a lowercase state key (so Home Assistant can look it up) or leave the
vendor's own casing alone as a readable fallback.
* ``laundry.cycle_select`` has to decide whether a device-reported course
table has translations at all before keying off it.
Reading it from the catalog instead of restating it in Python means adding a
state or a course table is a one-file change, and the two can't drift.
"""
from __future__ import annotations
import json
from pathlib import Path
# Read at import, not lazily: custom integrations are imported in an executor
# thread, so this stays off the event loop no matter who asks first.
_ENTITY_CATALOG: dict[str, dict[str, dict]] = json.loads(
(Path(__file__).parent / 'translations' / 'en.json').read_text(encoding='utf-8')
).get('entity', {})
def has_entity_translation(platform: str, translation_key: str) -> bool:
"""Whether `platform`.`translation_key` has an entry in the catalog."""
return translation_key in _ENTITY_CATALOG.get(platform, {})
def translated_states(platform: str, translation_key: str) -> frozenset[str]:
"""The state keys translated for `platform`.`translation_key`.
Empty when the entity has no translation at all, and also when it has a
translated *name* but deliberately no state table (a course list whose
codes we can't map, for instance) -- callers treat both the same way:
leave the device's value untouched.
"""
entry = _ENTITY_CATALOG.get(platform, {}).get(translation_key)
return frozenset(entry.get('state', ())) if entry else frozenset()
+198 -32
View File
@@ -20,10 +20,13 @@ back for, power, mode, temperature and wind resources alike.
"""
from __future__ import annotations
import logging
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
PRESET_NONE,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTemperature
@@ -45,6 +48,7 @@ from .registry.capabilities.airconditioner import (
HREF_TEMPS_VS as TEMPS_VS_HREF,
HREF_WIND_STRENGTH as WIND_STRENGTH_HREF,
HREF_WIND_DIRECTION as WIND_DIRECTION_HREF,
HREF_WIND_OSCILLATION as WIND_OSCILLATION_HREF,
HREF_CONVENIENT as CONVENIENT_HREF,
)
from .registry.capabilities.common import normalize_temp_unit
@@ -53,6 +57,8 @@ from .const import DOMAIN
from .coordinator import LocalThingsCoordinator
from .entity import LocalThingsEntity, _is_included
_LOGGER = logging.getLogger(__name__)
_MODES_FIELD = 'x.com.samsung.da.modes'
_SUPPORTED_FIELD = 'x.com.samsung.da.supportedModes'
@@ -62,12 +68,39 @@ _SUPPORTED_FIELD = 'x.com.samsung.da.supportedModes'
_DEVICE_TO_HVAC: dict[str, HVACMode] = {
'Cool': HVACMode.COOL,
'Dry': HVACMode.DRY,
# Fan-only is spelled 'Wind' on some boards (e.g. TP1X_DA-AC-RAC-01001) and
# 'Fan' on others (e.g. TP1X_DA-AC-RAC-01011); both map to FAN_ONLY. The
# reverse write can't rely on this map alone (two codes, one HA value) --
# _device_code_for_hvac() resolves the code from the unit's own
# supportedModes, so this is only a fallback for a unit reporting no
# supportedModes at all. 'Fan' is listed first so the {v: k} reverse
# comprehension below has 'Wind' win that fallback (last-key-wins),
# preserving the original single-spelling behavior rather than silently
# flipping it when 'Fan' was added.
'Fan': HVACMode.FAN_ONLY,
'Wind': HVACMode.FAN_ONLY,
'Auto': HVACMode.HEAT_COOL,
# The device's 'Auto' is a single-setpoint "device decides" mode -> HA
# HVACMode.AUTO (renders "Auto"). Not HEAT_COOL: that renders "Heat/cool"
# and implies a two-setpoint heat+cool range these single-setpoint units
# (including cool-only models) don't have.
'Auto': HVACMode.AUTO,
'Heat': HVACMode.HEAT,
}
_HVAC_TO_DEVICE = {v: k for k, v in _DEVICE_TO_HVAC.items()}
# AI-driven auto-comfort mode (issue #93, A-CAWW-TP2-20-COMMON). Not a flat
# _DEVICE_TO_HVAC entry: 'AIComfort' isn't a distinct thermodynamic operation
# like Cool/Dry/Heat, it's an AI overlay on top of the device's own 'Auto'
# behavior -- confirmed by this unit reporting both 'Auto' and 'AIComfort' as
# separate, mutually-exclusive entries in /mode/vs/0's supportedModes. Modeled
# the idiomatic HA way instead: hvac_mode reports AUTO (same as the plain
# 'Auto' code maps to) and a dedicated 'ai_comfort' preset carries the
# distinction a bare hvac_mode can't. Not reachable via async_set_hvac_mode --
# entered/left only through the preset, since there's no dedicated HVACMode
# value for it to write back to.
_AI_COMFORT_MODE = 'AIComfort'
PRESET_AI_COMFORT = 'ai_comfort'
# Fan (wind strength): device codes "0".."4" -> HA standard fan constants where
# a clean match exists so they auto-localize; "turbo" is custom (translated).
_DEVICE_TO_FAN: dict[str, str] = {
@@ -84,19 +117,42 @@ _DEVICE_TO_SWING: dict[str, str] = {
'Fix': 'off',
'All': 'both',
'Up_And_Low': 'vertical',
'Left_And_Right': 'horizontal', # issue #75
}
_SWING_TO_DEVICE = {v: k for k, v in _DEVICE_TO_SWING.items()}
# Preset (convenient mode): Off/Sleep map onto HA standard presets; Quiet/Smart/
# Speed are custom (translated).
_DEVICE_TO_PRESET: dict[str, str] = {
'Off': 'none',
'Sleep': 'sleep',
'Quiet': 'quiet',
'Smart': 'smart',
'Speed': 'speed',
}
_PRESET_TO_DEVICE = {v: k for k, v in _DEVICE_TO_PRESET.items()}
# Swing fallback via /wind/oscillation/vs/0 (issue #126) -- boards without
# WIND_DIRECTION_HREF at all report two independent Swing|Fix toggles
# instead of one combined code. Same HA vocabulary as _DEVICE_TO_SWING
# above (off/vertical/horizontal/both), just read from/written to a pair
# of fields rather than a single one.
def _oscillation_swing(rep: dict) -> str | None:
vertical = rep.get('vertical')
horizontal = rep.get('horizontal')
if vertical is None and horizontal is None:
return None
v = vertical == 'Swing'
h = horizontal == 'Swing'
if v and h:
return 'both'
if v:
return 'vertical'
if h:
return 'horizontal'
return 'off'
# 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
# exposed as its lowercased self and labelled in translations
# (entity.climate.airconditioner.state_attributes.preset_mode.state.<code>),
# so any board's convenient modes surface without code changes, and an
# unlabelled code just renders as its raw value until a label is added.
# (Samsung's WindFree still-air cooling shows up here as the 'Nano'/
# 'NanoSleep' codes on cool-only global RAC boards -- that's just a
# translation label, not a hard-coded mode.)
def _preset_to_ha(code) -> str:
return PRESET_NONE if code == 'Off' else str(code).lower()
async def async_setup_entry(
@@ -164,6 +220,11 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)
# (href, raw device code) pairs already logged by _warn_unmapped --
# these properties are read on every coordinator refresh, so an
# un-deduped warning would spam the log for any device with a
# genuinely unrecognized code.
self._warned_unmapped: set[tuple[str, str]] = set()
# -- resource helpers ---------------------------------------------------
@@ -171,25 +232,59 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
return self.coordinator.resource(href) or {}
def _is_on(self) -> bool:
rep = self._rep(POWER_HREF)
if 'value' in rep:
return bool(rep.get('value'))
vs = self._rep(POWER_VS_HREF)
return str(vs.get('x.com.samsung.da.power', '')).lower() == 'on'
# Prefer the vendor /power/vs/0 (present on every observed board and
# the resource writes target -- see airconditioner._climate_write).
# The OCF /power/0 is absent on many boards and a stale mirror on
# some, so reading it first showed pre-write state after a power
# toggle (issue #53: "can turn on but not off").
power = self._rep(POWER_VS_HREF).get('x.com.samsung.da.power')
if power is not None:
return str(power).lower() == 'on'
return bool(self._rep(POWER_HREF).get('value'))
def _supported(self, href: str) -> list[str]:
return list(self._rep(href).get(_SUPPORTED_FIELD) or [])
def _warn_unmapped(self, href: str, code: str) -> None:
"""Log once per (href, code) when a device-reported mode has no
entry in the relevant device<->HA map, so a real device gap surfaces
in the log instead of silently vanishing (issue #93)."""
key = (href, code)
if key in self._warned_unmapped:
return
self._warned_unmapped.add(key)
_LOGGER.warning(
"%s: device mode %r on %s has no HA mapping and was dropped; "
"please file an issue with your diagnostics dump",
self.entity_id, code, href,
)
def _read_mode(self, href: str, mapping: dict):
"""Current mode of a wind/convenient resource, mapped to its HA value."""
return mapping.get(_first(self._rep(href).get(_MODES_FIELD)))
raw = _first(self._rep(href).get(_MODES_FIELD))
if raw is not None and raw not in mapping:
self._warn_unmapped(href, raw)
return mapping.get(raw)
def _read_modes(self, href: str, mapping: dict) -> list[str]:
"""Supported modes of a resource, mapped to HA values (unknowns dropped)."""
return [mapping[c] for c in self._supported(href) if c in mapping]
supported = self._supported(href)
for c in supported:
if c not in mapping:
self._warn_unmapped(href, c)
return [mapping[c] for c in supported if c in mapping]
# -- temperature --------------------------------------------------------
def _ocf_temp_authoritative(self) -> bool:
"""True when the OCF /temperature/{current,desired}/0 pair is the
authoritative temperature channel -- signalled by
/temperature/current/0 being present. Those boards honour reads/
writes on /temperature/desired/0 and ignore the vendor
/temperatures/vs/0; boards without the pair (only a desired stub, or
nothing) are the reverse. Confirmed on live units of both kinds."""
return bool(self._rep(TEMP_CURRENT_HREF))
def _temps_vs(self) -> dict:
"""Vendor `/temperatures/vs/0` items[0] (empty {} when absent)."""
return _temps_vs_item(self._rep(TEMPS_VS_HREF))
@@ -212,10 +307,14 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
@property
def target_temperature(self):
v = _num(self._rep(TEMP_DESIRED_HREF).get('temperature'))
if v is None:
v = _num(self._temps_vs().get('x.com.samsung.da.desired'))
return v
# Read from the same channel writes go to (see async_set_temperature):
# OCF /temperature/desired/0 on boards with the full OCF pair, vendor
# /temperatures/vs/0 otherwise -- with the other as fallback.
ocf = _num(self._rep(TEMP_DESIRED_HREF).get('temperature'))
vs = _num(self._temps_vs().get('x.com.samsung.da.desired'))
if self._ocf_temp_authoritative():
return ocf if ocf is not None else vs
return vs if vs is not None else ocf
def _range(self) -> list | None:
r = self._rep(TEMP_DESIRED_HREF).get('range')
@@ -250,14 +349,23 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
if not self._is_on():
return HVACMode.OFF
device = _first(self._rep(MODE_HREF).get(_MODES_FIELD))
if device == _AI_COMFORT_MODE:
return HVACMode.AUTO
if device is not None and device not in _DEVICE_TO_HVAC:
self._warn_unmapped(MODE_HREF, device)
return _DEVICE_TO_HVAC.get(device, HVACMode.AUTO)
@property
def hvac_modes(self) -> list[HVACMode]:
modes = [HVACMode.OFF]
for m in self._supported(MODE_HREF):
if m == _AI_COMFORT_MODE:
continue
mapped = _DEVICE_TO_HVAC.get(m)
if mapped is not None and mapped not in modes:
if mapped is None:
self._warn_unmapped(MODE_HREF, m)
continue
if mapped not in modes:
modes.append(mapped)
return modes
@@ -271,34 +379,76 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
def fan_modes(self) -> list[str]:
return self._read_modes(WIND_STRENGTH_HREF, _DEVICE_TO_FAN)
def _swing_via_direction(self) -> bool:
"""True when WIND_DIRECTION_HREF is the swing channel to use --
signalled by its presence. Boards without it (issue #126) report
the 2-axis oscillation resource instead; see _oscillation_swing."""
return bool(self._rep(WIND_DIRECTION_HREF))
@property
def swing_mode(self):
return self._read_mode(WIND_DIRECTION_HREF, _DEVICE_TO_SWING)
if self._swing_via_direction():
return self._read_mode(WIND_DIRECTION_HREF, _DEVICE_TO_SWING)
return _oscillation_swing(self._rep(WIND_OSCILLATION_HREF))
@property
def swing_modes(self) -> list[str]:
return self._read_modes(WIND_DIRECTION_HREF, _DEVICE_TO_SWING)
if self._swing_via_direction():
return self._read_modes(WIND_DIRECTION_HREF, _DEVICE_TO_SWING)
if self._rep(WIND_OSCILLATION_HREF):
return list(_SWING_TO_DEVICE.keys())
return []
@property
def preset_mode(self):
return self._read_mode(CONVENIENT_HREF, _DEVICE_TO_PRESET)
if _first(self._rep(MODE_HREF).get(_MODES_FIELD)) == _AI_COMFORT_MODE:
return PRESET_AI_COMFORT
code = _first(self._rep(CONVENIENT_HREF).get(_MODES_FIELD))
return _preset_to_ha(code) if code is not None else None
@property
def preset_modes(self) -> list[str]:
return self._read_modes(CONVENIENT_HREF, _DEVICE_TO_PRESET)
modes = [_preset_to_ha(c) for c in self._supported(CONVENIENT_HREF)]
if _AI_COMFORT_MODE in self._supported(MODE_HREF):
modes.append(PRESET_AI_COMFORT)
return modes
# -- writes -------------------------------------------------------------
def _device_code_for_hvac(self, hvac_mode: HVACMode):
"""Device mode code for an HA hvac_mode, chosen from this unit's own
supportedModes -- fan-only is 'Wind' on some boards and 'Fan' on
others, so the reverse map alone can't pick the code this unit
accepts."""
for code in self._supported(MODE_HREF):
if _DEVICE_TO_HVAC.get(code) == hvac_mode:
return code
return _HVAC_TO_DEVICE.get(hvac_mode)
async def async_set_temperature(self, **kwargs) -> None:
# HA's set_temperature service forwards an optional hvac_mode here; honour
# it (set the mode first -- that also powers the unit on when it was off),
# matching the climate contract other integrations follow. Without this a
# set_temperature call carrying hvac_mode (e.g. a dashboard "turn on to
# Auto 24" button) set the setpoint but never changed mode or powered on.
hvac_mode = kwargs.get('hvac_mode')
if hvac_mode is not None:
await self.async_set_hvac_mode(hvac_mode)
if hvac_mode == HVACMode.OFF:
return
temp = kwargs.get('temperature')
if temp is not None:
await self.coordinator.async_send_command(self._bound, ('temperature', temp))
if temp is None:
return
# OCF-pair boards write /temperature/desired/0; vendor boards write
# /temperatures/vs/0 (see airconditioner._climate_write).
kind = 'temperature_ocf' if self._ocf_temp_authoritative() else 'temperature'
await self.coordinator.async_send_command(self._bound, (kind, temp))
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
if hvac_mode == HVACMode.OFF:
await self.coordinator.async_send_command(self._bound, ('power', False))
return
device = _HVAC_TO_DEVICE.get(hvac_mode)
device = self._device_code_for_hvac(hvac_mode)
if device is None:
return
if not self._is_on():
@@ -321,7 +471,23 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
await self._set_mapped('fan', _FAN_TO_DEVICE, fan_mode)
async def async_set_swing_mode(self, swing_mode: str) -> None:
await self._set_mapped('swing', _SWING_TO_DEVICE, swing_mode)
if self._swing_via_direction():
await self._set_mapped('swing', _SWING_TO_DEVICE, swing_mode)
return
if self._rep(WIND_OSCILLATION_HREF):
await self.coordinator.async_send_command(
self._bound, ('oscillation', swing_mode))
async def async_set_preset_mode(self, preset_mode: str) -> None:
await self._set_mapped('preset', _PRESET_TO_DEVICE, preset_mode)
if preset_mode == PRESET_AI_COMFORT:
# Writes the primary mode resource, not the convenient one --
# 'AIComfort' lives in /mode/vs/0 alongside Cool/Dry/Auto, not in
# /mode/convenient/vs/0 with Quiet/Smart/Speed/Sleep.
await self.coordinator.async_send_command(self._bound, ('mode', _AI_COMFORT_MODE))
return
# Reverse-resolve against the unit's own supportedModes (codes aren't
# a fixed transform of the HA value -- e.g. 'NanoSleep' -> 'nanosleep').
for code in self._supported(CONVENIENT_HREF):
if _preset_to_ha(code) == preset_mode:
await self.coordinator.async_send_command(self._bound, ('preset', code))
return
+39 -8
View File
@@ -197,6 +197,18 @@ def _find_live_ports(host: str, ports: list[int], timeout: float) -> list[int]:
return _order_candidates(live)
def _is_placeholder_serial(serial: str) -> bool:
"""True for a non-empty serialNum that isn't actually a real identity.
The ARTIK051_DONGLE_REF firmware family reports the literal string
'Nothing(SVC)' for every unit -- non-empty, so the plain `if not
serial` check here (and the equivalent one in coordinator.py's
`_run_discovery`) doesn't catch it, and two such units get the same
config-entry unique_id / entity unique_ids and collide (issue #83).
"""
return serial.strip().lower().startswith('nothing')
def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict:
"""Fetch UUID, mint leaf cert, probe each port. Returns config entry data dict."""
import cbor2
@@ -261,7 +273,7 @@ def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict:
.get('/information/vs/0', {})
.get('x.com.samsung.da.serialNum', '')
)
if not serial:
if not serial or _is_placeholder_serial(serial):
serial = f"{host}:{port}"
one_ui_version = (
resources
@@ -368,22 +380,27 @@ class LocalThingsConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if has_creds:
schema = vol.Schema({vol.Required(CONF_HOST): _TEXT})
reuse_note = "CA credentials will be reused from the existing device."
step_id = "user_reuse"
else:
schema = vol.Schema({
vol.Required(CONF_HOST): _TEXT,
vol.Required(CONF_CA_CERT_PEM): _MULTILINE,
vol.Required(CONF_CA_KEY_PEM): _MULTILINE,
})
reuse_note = ""
step_id = "user"
return self.async_show_form(
step_id="user",
step_id=step_id,
data_schema=schema,
errors=errors,
description_placeholders={"reuse_note": reuse_note},
)
async def async_step_user_reuse(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the localized host-only form for additional appliances."""
return await self.async_step_user(user_input)
async def async_step_confirm_unknown_type(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -391,14 +408,28 @@ class LocalThingsConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None:
return self._create_entry(self._pending_info)
if not self._pending_info["one_ui_version"]:
return await self.async_step_confirm_unknown_type_no_version()
return self.async_show_form(
step_id="confirm_unknown_type",
data_schema=vol.Schema({}),
description_placeholders={
"one_ui_version": self._pending_info["one_ui_version"] or "(none reported)",
"one_ui_version": self._pending_info["one_ui_version"],
},
)
async def async_step_confirm_unknown_type_no_version(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm an unknown appliance that did not report oneUiVersion."""
if user_input is not None:
return self._create_entry(self._pending_info)
return self.async_show_form(
step_id="confirm_unknown_type_no_version",
data_schema=vol.Schema({}),
)
class LocalThingsOptionsFlow(config_entries.OptionsFlow):
"""Per-device options: the remote-control-off write-block override
@@ -487,7 +518,7 @@ class LocalThingsOptionsFlow(config_entries.OptionsFlow):
"href": href,
"current_value": (
json.dumps(current, indent=2, ensure_ascii=False)
if current else "(no cached value)"
if current else "{}"
),
},
)
@@ -531,7 +562,7 @@ class LocalThingsOptionsFlow(config_entries.OptionsFlow):
"code": f"{code >> 5}.{code & 0x1f:02d} ({code:#04x})",
"new_value": (
json.dumps(new_rep, indent=2, ensure_ascii=False)
if new_rep else "(no value returned)"
if new_rep else "{}"
),
},
)
+111 -27
View File
@@ -22,7 +22,12 @@ from smartthings_local.ocf.state_cache import StateCache
from .registry.batch import parse_device0_batch
from .registry.by_type import for_device, for_device_by_model, for_device_by_resources
from .registry.capabilities.common import merge_options_field, remote_control_enabled
from .registry.capabilities.common import (
merge_items_field,
merge_options_field,
remote_control_enabled,
remote_control_required_for_write,
)
from .registry.discovery import discover, BoundEntity
from .registry import CAPABILITIES
from .registry.adapter import flatten
@@ -38,13 +43,6 @@ _LOGGER = logging.getLogger(__name__)
_SEED_PATH = ['device', '0']
_REMOTE_CONTROL_DISABLED_MESSAGE = (
"Remote control is turned off on this device. Check your appliance's "
"manual for how to enable remote control before Home Assistant can "
"control it."
)
class _NoOpDescriptor:
"""StateCache requires a descriptor with an on_observation hook. This
integration doesn't use per-capability observation hooks, so this is a
@@ -56,6 +54,22 @@ class _NoOpDescriptor:
_RECOVERY_RETRY_S = 600.0 # re-attempt observe mode this often while polling
def _is_placeholder_serial(serial: str) -> bool:
"""True for a non-empty serialNum that isn't actually a real identity.
The ARTIK051_DONGLE_REF firmware family reports the literal string
'Nothing(SVC)' for every unit -- non-empty, so the plain `if not
serial` check below doesn't catch it, and `device_serial` feeds both
the HA device-registry identifier and every entity's unique_id
(entity.py), so two such units on the same install silently collide
and the second one's entities get dropped (issue #83). Mirrors the
identical helper in config_flow.py's `_probe_and_validate` -- kept
separate rather than imported to avoid pulling the config-flow module
into the runtime coordinator's import graph for a two-line check.
"""
return serial.strip().lower().startswith('nothing')
class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Manages one Samsung appliance: session, discovery, polling."""
@@ -71,6 +85,24 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
_OBSERVE_GRACE_PERIOD_S: float = GRACE_PERIOD_S
_RECONNECT_PAUSE_S: float = 5.0
# A single reconnect is normal appliance-side behavior (see the
# README's "Known device behavior" section) -- Samsung's firmware
# drops the DTLS session briefly every now and then, and the
# coordinator recovering from that on its own isn't something a user
# needs to see at WARNING. Only escalate once reconnects pile up
# within a trailing window (issue #119).
#
# The window can't be a literal 60s: consecutive reconnect attempts are
# never closer together than one summary poll interval (SUMMARY_INTERVAL_S,
# 30s) plus _RECONNECT_PAUSE_S, so at most ~2 can ever land inside a 60s
# window regardless of how unhealthy the connection is -- a threshold of
# 5 there could never fire, silently downgrading every reconnect
# (including a persistently broken one) to INFO forever. 300s/3 instead:
# reachable under normal polling, and 3 reconnects inside 5 minutes is
# still a reasonable proxy for the README's "actually broken" case.
_RECONNECT_WARN_WINDOW_S: float = 300.0
_RECONNECT_WARN_THRESHOLD: int = 3
# A block-level ACK timeout on the summary GET doesn't prove the
# session is dead (see _poll_once) — require this many in a row
# before treating it as one. A single slow transfer on an otherwise
@@ -122,6 +154,7 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self.one_ui_version: str = ''
self._consecutive_poll_timeouts = 0
self._unbound_hrefs: list[str] = []
self._reconnect_times: list[float] = []
# ------------------------------------------------------------------
# Session management (all blocking — must run in executor)
@@ -319,7 +352,7 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._unbound_hrefs = unbound
serial = info.get('x.com.samsung.da.serialNum', '')
if not serial:
if not serial or _is_placeholder_serial(serial):
serial = self._entry.data[CONF_HOST]
self.device_serial = serial
@@ -430,6 +463,17 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._consecutive_poll_timeouts += 1
return self._consecutive_poll_timeouts < self._POLL_TIMEOUT_LIMIT
def _reconnect_is_frequent(self) -> bool:
"""True once this cycle's reconnect is the Nth within the trailing
warn window -- see _RECONNECT_WARN_WINDOW_S/_RECONNECT_WARN_THRESHOLD."""
now = time.monotonic()
self._reconnect_times = [
t for t in self._reconnect_times
if now - t < self._RECONNECT_WARN_WINDOW_S
]
self._reconnect_times.append(now)
return len(self._reconnect_times) >= self._RECONNECT_WARN_THRESHOLD
# ------------------------------------------------------------------
# DataUpdateCoordinator hook
# ------------------------------------------------------------------
@@ -456,7 +500,13 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._consecutive_poll_timeouts = 0
# One reconnect attempt — pause briefly so the device can
# clean up its DTLS session state before we knock again.
self._log.warning("poll failed, reconnecting: %s", e)
# A lone reconnect is routine (see the README's "Known
# device behavior" section); only warn once they're piling
# up within the trailing window.
if self._reconnect_is_frequent():
self._log.warning("poll failed, reconnecting: %s", e)
else:
self._log.info("poll failed, reconnecting: %s", e)
await self.hass.async_add_executor_job(self._close_session)
await asyncio.sleep(self._RECONNECT_PAUSE_S)
try:
@@ -538,7 +588,10 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
opted this device out of it via CONF_BYPASS_REMOTE_CONTROL (issue
#54: some devices accept certain writes, e.g. a washer's default
dosing levels, even while reporting remote control off, so the
block's assumption doesn't hold for every model)."""
block's assumption doesn't hold for every model), or the laundry
firmware flag isModelSettingWithoutSC declares settings writable
without Smart Control (cycle start/pause/stop on /operational/state
still require it)."""
desc = bound_entity.desc
write_fn = getattr(desc, 'write_fn', None)
if write_fn is None:
@@ -547,14 +600,27 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
rep = self._cache.get(href or '') or {}
resources = self._cache.snapshot()
bypass_remote_control = self._entry.options.get(CONF_BYPASS_REMOTE_CONTROL, False)
if not bypass_remote_control and not remote_control_enabled(resources):
raise ServiceValidationError(_REMOTE_CONTROL_DISABLED_MESSAGE)
if (
not bypass_remote_control
and remote_control_required_for_write(resources, href or '')
and not remote_control_enabled(resources)
):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="remote_control_disabled",
)
validate_fn = getattr(desc, 'validate_fn', None)
if validate_fn is not None:
error = validate_fn(payload, rep, resources)
if error:
raise ServiceValidationError(error)
result = write_fn(payload, rep, href)
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key=error,
)
try:
result = write_fn(payload, rep, href, resources)
except TypeError:
result = write_fn(payload, rep, href)
if result is None:
self._log.warning("write_fn rejected payload %r for %s", payload, href)
return
@@ -612,23 +678,35 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# introduced its own races around overlapping writes to the same
# href. Simpler and safer to just hold the guard for the full,
# generously-sized window and let it expire on its own.
# write_fn bodies that touch x.com.samsung.da.options carry only the
# changed token(s) now (issue #54: confirmed sufficient on the wire --
# the device merges by prefix itself), not the whole packed array.
# observe.apply()'s field-level {**cached, **rep} merge doesn't know
# that -- handed the bare token list, it would replace the cached
# field outright and wipe every sibling option for the rest of the
# settle window. Pre-merge it here the same way the device does, so
# the optimistic cache entry stays complete; the minimal `body` below
# is still exactly what goes out over the wire.
# write_fn bodies that touch x.com.samsung.da.options or
# x.com.samsung.da.items carry only the changed token(s)/item now
# (issue #54 for options; the AC vendor temperature write for items --
# confirmed sufficient on the wire, the device merges the rest itself),
# not the whole packed array. observe.apply()'s field-level
# {**cached, **rep} merge doesn't know that -- handed the bare
# partial value, it would replace the cached field outright and wipe
# every sibling option/item for the rest of the settle window.
# Pre-merge it here the same way the device does, so the optimistic
# cache entry stays complete; the minimal `body` below is still
# exactly what goes out over the wire.
optimistic_body = body
new_options = body.get('x.com.samsung.da.options')
if isinstance(new_options, list):
cached_options = (self._cache.get(write_href) or {}).get('x.com.samsung.da.options')
optimistic_body = {
**body,
**optimistic_body,
'x.com.samsung.da.options': merge_options_field(cached_options, new_options),
}
# Same fact, items[] shape (e.g. airconditioner._climate_write's vendor
# temperature write, which now carries only {id, desired} -- see that
# module for the write-side half of this).
new_items = body.get('x.com.samsung.da.items')
if isinstance(new_items, list):
cached_items = (self._cache.get(write_href) or {}).get('x.com.samsung.da.items')
optimistic_body = {
**optimistic_body,
'x.com.samsung.da.items': merge_items_field(cached_items, new_items),
}
self._observe.apply(write_href, optimistic_body, source='optimistic')
self._observe.mark_write_pending(
write_href, settle_s=self._POST_TIMEOUT_S + self._POLL_TIMEOUT_S
@@ -689,10 +767,16 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
help users pin down device-specific write behavior without a new
release."""
if not isinstance(body, dict) or not body:
raise ServiceValidationError("Debug write payload must be a non-empty object.")
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="debug_payload_empty",
)
path_segs = [s for s in href.strip('/').split('/') if s]
if not path_segs:
raise ServiceValidationError("A resource href is required.")
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="resource_href_required",
)
norm_href = '/' + '/'.join(path_segs)
async with self._session_lock:
code, new_rep = await self.hass.async_add_executor_job(
+42 -16
View File
@@ -38,15 +38,32 @@ def _is_included(bound: BoundEntity, coordinator: 'LocalThingsCoordinator') -> b
def _derive_name(state_key: str) -> str:
"""Turn a snake_case state key into a title-cased display name.
"""Turn a snake_case state key into a title-cased label.
Strips a trailing instance number of 0 (singleton), promotes any other
instance number with a space: "door_cooler_open1" → "Door Cooler Open 1".
Entity names themselves come from the translation catalog; this only
builds the {instance_name} placeholder those translations interpolate,
for a device that named its own compartments/ice makers.
"""
name = re.sub(r'(\d+)$', lambda m: f' {m.group()}' if int(m.group()) > 0 else '', state_key)
return _snake_to_title(name).strip()
def _instance_display_name(bound: BoundEntity, state_key: str) -> str:
"""Return the stable vendor/href instance label used in a name placeholder."""
if bound.instance_name:
return bound.instance_name
source = bound.key_override or state_key
suffix = f"_{bound.desc.key}"
if source.endswith(suffix):
source = source[:-len(suffix)]
elif bound.instance and source.endswith(bound.instance):
source = source[:-len(bound.instance)] + bound.instance.replace("_", " ")
return _derive_name(source)
class LocalThingsEntity(CoordinatorEntity[LocalThingsCoordinator]):
"""Base class for all Local Things entities."""
@@ -57,15 +74,21 @@ class LocalThingsEntity(CoordinatorEntity[LocalThingsCoordinator]):
self._bound = bound
self._state_key = _key(bound)
self._attr_unique_id = f"{DOMAIN}_{coordinator.device_serial}_{self._state_key}"
if bound.desc.name is not None:
self._attr_name = bound.desc.name
elif bound.instance_name:
# A device-given instance name (e.g. an ice maker's "Cubed
# Ice") takes the place of the href-derived instance label,
# keeping the same entity-specific suffix (issue #27).
self._attr_name = f"{bound.instance_name} {_derive_name(bound.desc.key)}".strip()
else:
self._attr_name = _derive_name(self._state_key)
if bound.desc.translation_placeholders is not None:
self._attr_translation_placeholders = dict(
bound.desc.translation_placeholders
)
elif bound.desc.use_instance_name:
self._attr_translation_placeholders = {
"instance_name": _instance_display_name(bound, self._state_key)
}
# _attr_name is deliberately left unset: Home Assistant gives an
# explicitly-set name precedence over the translation catalog, so
# setting it here would make every entity untranslatable. Every
# descriptor resolves to a catalog entry (see translation_key below);
# a platform that wants the bare device name instead sets
# _attr_name = None itself, as fan.py does for the hood's main entity.
self._attr_icon = bound.desc.icon
raw_cat = bound.desc.entity_category
self._attr_entity_category = EntityCategory(raw_cat) if raw_cat else None
@@ -73,11 +96,12 @@ class LocalThingsEntity(CoordinatorEntity[LocalThingsCoordinator]):
@property
def translation_key(self) -> str | None:
"""Override Entity.translation_key (a property upstream, not a
plain attribute) so a callable descriptor -- e.g.
laundry.cycle_select's table-id-gated resolver -- is re-evaluated
against live coordinator data on every access, not resolved once
at construction time.
"""The descriptor's catalog key, defaulting to its own `key`.
Overrides Entity.translation_key (a property upstream, not a plain
attribute) so a callable descriptor -- e.g. laundry.cycle_select's
table-id-gated resolver -- is re-evaluated against live coordinator
data on every access, not resolved once at construction time.
Discovery runs on the first /device/0 poll, which the entity
registry already documents can hand a sibling resource an empty
@@ -88,7 +112,9 @@ class LocalThingsEntity(CoordinatorEntity[LocalThingsCoordinator]):
value arrives on a later poll.
"""
tk = self._bound.desc.translation_key
return tk(self.coordinator.last_resources) if callable(tk) else tk
if callable(tk):
return tk(self.coordinator.last_resources)
return tk if tk is not None else self._bound.desc.key
@property
def device_info(self) -> DeviceInfo:
+100 -6
View File
@@ -1,7 +1,15 @@
"""Fan platform for Samsung range hoods."""
"""Fan platform for Samsung range hoods and air purifiers.
Two FanDesc-bound hrefs exist, one per family, dispatched by href in
async_setup_entry below since they need different HA fan semantics: the
range hood's fan speed is an ordered set of numeric levels (SET_SPEED), while
the newer air-purifier board family's modes (Smart/Max/Mid/WindFree/Sleep,
issue #130) are named behaviors with no linear order (PRESET_MODE)."""
from __future__ import annotations
import logging
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -14,14 +22,19 @@ from homeassistant.util.percentage import (
from .const import DOMAIN
from .coordinator import LocalThingsCoordinator
from .entity import LocalThingsEntity, _is_included
from .registry.capabilities.air_purifier import HREF_MODE as AIR_PURIFIER_FAN_HREF
from .registry.entities import FanDesc
_LOGGER = logging.getLogger(__name__)
POWER_HREF = '/power/0'
POWER_VS_HREF = '/power/vs/0'
_FAN_SPEED_FIELD = 'x.com.samsung.da.hood.fanSpeed'
_SUPPORTED_FAN_SPEED_FIELD = 'x.com.samsung.da.hood.supportedFanSpeed'
_MODES_FIELD = 'x.com.samsung.da.modes'
_SUPPORTED_MODES_FIELD = 'x.com.samsung.da.supportedModes'
async def async_setup_entry(
hass: HomeAssistant,
@@ -29,11 +42,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
LocalThingsRangeHoodFan(coordinator, bound)
for bound in coordinator.bound
if isinstance(bound.desc, FanDesc) and _is_included(bound, coordinator)
)
entities = []
for bound in coordinator.bound:
if not (isinstance(bound.desc, FanDesc) and _is_included(bound, coordinator)):
continue
if bound.href == AIR_PURIFIER_FAN_HREF:
entities.append(LocalThingsAirPurifierFan(coordinator, bound))
else:
entities.append(LocalThingsRangeHoodFan(coordinator, bound))
async_add_entities(entities)
class LocalThingsRangeHoodFan(LocalThingsEntity, FanEntity):
@@ -120,3 +137,80 @@ class LocalThingsRangeHoodFan(LocalThingsEntity, FanEntity):
)
code = percentage_to_ordered_list_item(codes, percentage)
await self.coordinator.async_send_command(self._bound, ('speed', code))
class LocalThingsAirPurifierFan(LocalThingsEntity, FanEntity):
"""Air-purifier fan: named preset modes, not an ordered percentage --
see air_purifier.FAN's comment for why (Smart/WindFree/Sleep aren't
"faster/slower" than Max/Mid)."""
_enable_turn_on_off_backwards_compatibility = False
_attr_supported_features = (
FanEntityFeature.PRESET_MODE
| FanEntityFeature.TURN_ON
| FanEntityFeature.TURN_OFF
)
def __init__(self, coordinator: LocalThingsCoordinator, bound) -> None:
super().__init__(coordinator, bound)
self._attr_name = None
def _rep(self, href: str) -> dict:
return self.coordinator.resource(href) or {}
def _mode_rep(self) -> dict:
return self._rep(self._bound.href)
def _power_payload(self, enabled: bool) -> tuple[str, bool, str]:
"""Target whichever power resource this unit actually exposes --
same pattern as LocalThingsRangeHoodFan._power_payload above.
Writing a hardcoded href here would silently no-op on a board that
only reports the other one, even though is_on already falls back
correctly."""
resources = self.coordinator.last_resources
target = POWER_VS_HREF if POWER_VS_HREF in resources else POWER_HREF
return 'power', enabled, target
@property
def is_on(self) -> bool:
power = self._rep(POWER_VS_HREF).get('x.com.samsung.da.power')
if power is not None:
return str(power).lower() == 'on'
return bool(self._rep(POWER_HREF).get('value'))
@property
def preset_modes(self) -> list[str]:
return [
str(code).lower()
for code in self._mode_rep().get(_SUPPORTED_MODES_FIELD, ())
]
@property
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
async def async_turn_on(
self, percentage: int | None = None, preset_mode: str | None = None,
**kwargs,
) -> None:
await self.coordinator.async_send_command(self._bound, self._power_payload(True))
if preset_mode is not None:
await self.async_set_preset_mode(preset_mode)
async def async_turn_off(self, **kwargs) -> None:
await self.coordinator.async_send_command(self._bound, self._power_payload(False))
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.
for code in self._mode_rep().get(_SUPPORTED_MODES_FIELD, ()):
if str(code).lower() == preset_mode:
await self.coordinator.async_send_command(self._bound, ('mode', code))
return
_LOGGER.warning(
"%s: %r is not a valid preset mode (supported: %s)",
self.entity_id, preset_mode, self.preset_modes,
)
+1 -1
View File
@@ -12,5 +12,5 @@
"pyOpenSSL>=23.0",
"smartthings-local>=0.1.0"
],
"version": "0.11.1"
"version": "0.13.0"
}
@@ -3,8 +3,9 @@ from typing import Optional
from ._base import DeviceRegistry
from . import (
air_purifier, airconditioner, cooktop, dishwasher, dryer, oven,
range as _range, range_hood, refrigerator, washer,
air_purifier, airconditioner, cooktop, dehumidifier, dishwasher, dryer,
induction_cooktop, oven, range as _range, range_hood, refrigerator,
vacuum_station, washer, water_purifier,
)
__all__ = [
@@ -19,14 +20,18 @@ _REGISTRY_BY_KEY: dict[str, DeviceRegistry] = {
'airconditioner': airconditioner.REGISTRY,
'air_conditioner': airconditioner.REGISTRY,
'cooktop': cooktop.REGISTRY,
'dehumidifier': dehumidifier.REGISTRY,
'dishwasher': dishwasher.REGISTRY,
'dryer': dryer.REGISTRY,
'induction_cooktop': induction_cooktop.REGISTRY,
'oven': oven.REGISTRY,
'hood': range_hood.REGISTRY,
'range': _range.REGISTRY,
'range_hood': range_hood.REGISTRY,
'refrigerator': refrigerator.REGISTRY,
'vacuum_station': vacuum_station.REGISTRY,
'washer': washer.REGISTRY,
'water_purifier': water_purifier.REGISTRY,
}
@@ -82,11 +87,55 @@ _CONSUMER_PREFIX_TO_KEY: dict[str, str] = {
'WD': 'washer',
'WF': 'washer',
'WV': 'washer', # FlexWash twin units (e.g. WV55M9600AW) -- issue #19
'WA': 'washer', # Top-load washers (e.g. WA8000T) -- issue #106
'DV': 'dryer',
'DW': 'dishwasher',
}
def _model_num_segments(model_num: str) -> list[str]:
"""Underscore-delimited segments of modelNum's pipe-prefix.
Most boards wrap a token in underscores on both sides ('..._REF_...'),
which a plain substring check catches fine. But the ARTIK051_DONGLE_REF
family (issues #77, #83) reports modelNum as
'<board>_DONGLE_REF|<rest...>' -- REF is the *last* segment before the
pipe, with no trailing underscore, so '_REF_' never matches and the
device silently fell back to 'unknown'. Splitting on '_' and checking
segment membership catches both shapes without caring which side (if
either) has a delimiter.
"""
prefix = (model_num or '').split('|', 1)[0]
return prefix.split('_')
def _consumer_model_key(description: str) -> Optional[str]:
"""Registry key from the consumer-model token in `description`, or None.
Usually that token is the last '_'-delimited segment before any
'/board-info' suffix (e.g. '..._WW90DG6U25LEU4' -> 'WW90DG6U25LEU4').
But issue #79's dryer pairs two model numbers in one description --
'..._DVE50A8800_8600/DC92-...' -- so the true consumer token
('DVE50A8800') sits one segment *before* the actual last segment
('8600', a bare second model number with no recognizable prefix). Scan
segments from the end and take the first one that resolves, rather
than assuming the last segment is always it.
Only a 2-letter *prefix* match -- e.g. 'WAC' (the Window Air Conditioner
board-family token, issue #87) also starts with 'WA' (the top-load-washer
prefix, issue #106) at this granularity. for_device_by_model() calls the
board-family modelNum checks first and this function only as a fallback,
so that ambiguity resolves correctly without this function needing to
know about unrelated device families.
"""
segments = (description or '').split('/', 1)[0].split('_')
for segment in reversed(segments):
key = _CONSUMER_PREFIX_TO_KEY.get(segment[:2].upper())
if key is not None:
return key
return None
def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegistry]:
"""Fallback device-type detection for hardware that never reports
oneUiVersion (confirmed for washers -- their /otninformation/vs/0 has
@@ -100,18 +149,30 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
DeviceRegistry if the consumer-model code or modelNum resolves to a
known type, None otherwise.
"""
token = (description or '').split('/', 1)[0].rsplit('_', 1)[-1]
key = _CONSUMER_PREFIX_TO_KEY.get(token[:2].upper())
if key is None and '_REF_' in (model_num or ''):
# Board-family modelNum tokens are checked first, and the fuzzier
# 2-letter consumer-model prefix from `description` only as a fallback
# (see the bottom of this function) -- some board tokens are themselves
# only 2-3 letters long ('WAC', issue #87) and would otherwise collide
# with an unrelated consumer prefix at that granularity ('WA', issue #106).
key = None
if key is None and 'REF' in _model_num_segments(model_num):
key = 'refrigerator'
# Room air conditioners (e.g. ARTIK051_PRAC_20K) report no oneUiVersion and
# a modelNum carrying the '_PRAC_' (Package Room Air Conditioner) token.
if key is None and '_PRAC_' in (model_num or ''):
key = 'airconditioner'
# Older/simpler RAC boards (e.g. TP2X_RAC_20K, issue #37) use the plain
# '_RAC_' token instead -- distinct from '_PRAC_' above (no overlap: the
# 'P' sits between the underscore and 'RAC' in that token).
if key is None and '_RAC_' in (model_num or ''):
# Other RAC boards carry a bare 'RAC' (Room Air Conditioner) token in the
# modelNum, in one of two spellings: the underscore form '_RAC_' (e.g.
# TP2X_RAC_20K, issue #37) or the hyphenated form '-RAC-' (e.g.
# TP1X_DA-AC-RAC-01001, a cool-only global variant, issue #91). Both are
# distinct from '_PRAC_' above ('P' sits before 'RAC' with no delimiter)
# and from range ('-RANGE-') / oven ('-OVEN-') tokens. Most TP1X boards
# self-report oneUiVersion and resolve via for_device() upstream of this
# fallback; the hyphenated match is what rescues variants whose
# /otninformation/vs/0 ships no swVersionInfo block at all, so
# oneUiVersion is empty.
if key is None and ('_RAC_' in (model_num or '')
or '-RAC-' in (model_num or '').upper()):
key = 'airconditioner'
# System air conditioners (multi-indoor-unit commercial installs, e.g.
# A-CAWW-TP2-20-COMMON, issue #52) report no oneUiVersion either and
@@ -122,6 +183,32 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
# resource (see airconditioner.py's _AC_IGNORED).
if key is None and '-CAWW-' in (model_num or '').upper():
key = 'airconditioner'
# Dehumidifiers (e.g. TP1X_DA_AC_DHM_01001_0000, issue #88) share the
# DA_AC_ board family with the room-AC models above but carry the
# '_DHM_' (DeHuMidifier) token instead of '_RAC_'/'_PRAC_'. Distinct
# registry: target humidity, not temperature, is the primary control,
# and there's no climate composite.
if key is None and '_DHM_' in (model_num or ''):
key = 'dehumidifier'
# Window air conditioners (e.g. TP1X_DA_AC_WAC_01001_0000, issue #87)
# report no oneUiVersion and carry the '_WAC_' (Window Air Conditioner)
# token instead of '_RAC_'/'_PRAC_'. Same TP1X-class resource surface
# as the room-AC models above (mode/convenient/wind/temperature/power/
# filter/humidity all confirmed against the issue #87 dump binding
# cleanly against the existing airconditioner registry once routed
# here) -- no WAC-specific resources needed.
if key is None and '_WAC_' 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
# surface as the other TP1X-class room ACs above (mode/convenient/wind/
# temperature/power/filter/humidity all confirmed against these dumps
# binding cleanly against the existing airconditioner registry once
# routed here) -- no ARA-WW-specific resources needed, so this reuses
# the same registry rather than adding a new device type.
if key is None and 'ARA-WW-' in (model_num or '').upper():
key = 'airconditioner'
# Air purifiers (e.g. ARTIK051_TVTL_18K, issue #56) report no
# oneUiVersion either, and carry the '_TVTL_' board-family token.
if key is None and '_TVTL_' in (model_num or ''):
@@ -129,6 +216,10 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
model_identity = f'{model_num} {description}'.upper()
if key is None and ('_COOKTOP' in model_identity or '_GB_CT_' in model_identity):
key = 'cooktop'
# Water purifiers (e.g. TP2X_WATERPURIFIER_20K, issue #90) report no
# oneUiVersion and carry the 'WATERPURIFIER' board-family token.
if key is None and 'WATERPURIFIER' in model_identity:
key = 'water_purifier'
if key is None and model_identity.startswith('AHD-'):
key = 'range_hood'
# Range/cooktop-oven combos (e.g. TP1X_DA-KS-RANGE-0102X, issue #44) --
@@ -141,6 +232,37 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
# oneUiVersion and doesn't match the washer/dryer/dishwasher prefix map.
if key is None and '-OVEN-' in (model_num or '').upper():
key = 'oven'
# Combi microwaves (e.g. TP1X_DA-KS-MICROWAVE-01041, issue #121) -- same
# board family as the wall oven above (an '/oven/vs/0' cavity resource
# with Convection/AirFryer/Grill/MicroWave modes on /mode/vs/0), just a
# different cavity. Reports no oneUiVersion and doesn't match the
# washer/dryer/dishwasher prefix map either. Binds cleanly against the
# existing oven registry once routed here (confirmed against the issue
# #121 dump) -- no microwave-specific device type needed.
if key is None and '-MICROWAVE-' in (model_num or '').upper():
key = 'oven'
# Standalone induction cooktops (e.g. TP1X_DA-KS-COOKTOP-01011, issue
# #86) -- same board family and '/cooktop/status/vs/0' resource shape
# as the range combo above, but no oven attached at all. Distinct from
# the '_COOKTOP'/'_GB_CT_' underscore-delimited check above: that one
# matches a different, older gas-cooktop family (cooktop.py's NA9300K
# class, burner state embedded in /mode/vs/0's options array) whose
# modelNum token is underscore-delimited, not hyphenated like this
# board family's.
if key is None and '-COOKTOP-' in (model_num or '').upper():
key = 'induction_cooktop'
# Stick-vacuum clean/auto-empty station (e.g. A-VSKR-TP1-22-VS9500AL,
# issue #131) -- reports no oneUiVersion and carries the '-VSKR-'
# board-family token. See capabilities/vacuum_station.py for why this
# is its own device type: the resource set (dustbag/dustbin/UV-sanitize
# station state) shares no hrefs with anything else already modeled.
if key is None and '-VSKR-' in (model_num or '').upper():
key = 'vacuum_station'
# 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.
if key is None:
key = _consumer_model_key(description)
return _REGISTRY_BY_KEY.get(key) if key else None
@@ -170,4 +292,18 @@ def for_device_by_resources(resources: dict[str, dict]) -> Optional[DeviceRegist
and '/hood/lamp/vs/0' in resources
):
return _REGISTRY_BY_KEY['range_hood']
# Oven/range-combo boards that report no /information/vs/0 at all
# (issue #74's NE63B8411SS -- the resource is simply absent from the
# dump, not just empty) can't be matched via for_device_by_model's
# modelNum tokens either. 'Bake' is oven/range cook-mode vocabulary that
# no other family's /mode/vs/0 uses (confirmed against the microwave,
# cooktop, and every laundry fixture), so its presence alongside the
# oven cavity resource is a safe signature. Distinguish range (has a
# cooktop half) from a plain wall oven by which cooktop-status resource,
# if any, is also present.
supported_modes = mode.get('x.com.samsung.da.supportedModes') or ()
if '/oven/vs/0' in resources and 'Bake' in supported_modes:
if '/cooktopmonitoring/vs/0' in resources or '/cooktop/status/vs/0' in resources:
return _REGISTRY_BY_KEY['range']
return _REGISTRY_BY_KEY['oven']
return None
@@ -1,10 +1,22 @@
"""Air-purifier device registry (Samsung ARTIK051_TVTL-class, issue #56).
"""Air-purifier device registry.
Reports no oneUiVersion; resolved via for_device_by_model's '_TVTL_' modelNum
token (see registry.py). Reuses dishwasher.DIAGNOSIS for /diagnosis/vs/0
(identical field/write contract).
Spans two 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):
- ARTIK051_TVTL-class (issue #56). Reports no oneUiVersion; resolved via
for_device_by_model's '_TVTL_' modelNum token (see registry.py).
- TP1X_DA-AC-AIR-class (issue #130). Self-reports oneUiVersion "7.0 Air
purifier", resolved via for_device(). Adds real fan-mode control plus
display/HEPA-filter/pet-filter/sound resources the older family never
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.
Reuses dishwasher.DIAGNOSIS for /diagnosis/vs/0 (identical field/write
contract).
"""
from ..capabilities import air_purifier, common, dishwasher, ignored
from ..capabilities import air_purifier, airconditioner, common, dishwasher, ignored
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
@@ -20,6 +32,16 @@ REGISTRY = DeviceRegistry(
air_purifier.AIRFLOW_GENERIC,
air_purifier.AIRFLOW_VS_FALLBACK,
air_purifier.MODE,
air_purifier.FAN,
air_purifier.DISPLAY,
air_purifier.HEPA_FILTER,
air_purifier.PANEL_STATUS,
air_purifier.PET_FILTER_ACTIVATION,
air_purifier.SOUND_MODE,
air_purifier.SOUND_OUTPUT,
air_purifier.SOUND_VOLUME,
airconditioner.DISPLAY_LIGHT,
airconditioner.MUTE_ONCE,
*air_purifier.COVERAGE,
]),
)
@@ -25,6 +25,10 @@ REGISTRY = DeviceRegistry(
airconditioner.DISPLAY_LIGHT,
airconditioner.MUTE_ONCE,
airconditioner.CURRENT_LIMIT,
airconditioner.ANOMALY_LOAD,
airconditioner.CURRENT_TEMPERATURE,
airconditioner.CURRENT_TEMPERATURE_VS,
airconditioner.HUMIDITY,
*airconditioner.COVERAGE,
]),
)
@@ -1,11 +1,21 @@
"""Cooktop device registry."""
"""Gas cooktop device registry (NA9300K-class, PR #23).
Named 'gas_cooktop' (not 'cooktop') so its diagnostics/device-info label
doesn't collide with the unrelated induction_cooktop family (issue #86,
by_type/induction_cooktop.py) -- two different OCF surfaces that happen to
share the English word "cooktop". The `_REGISTRY_BY_KEY['cooktop']` lookup
key is unchanged: it's relied on by real devices reporting
oneUiVersion "Cooktop" (for_device), the legacy ARTIK051 modelNum rule
(for_device_by_model), and the resource-signature fallback
(for_device_by_resources) alike.
"""
from ..capabilities import common, cooktop, ignored
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
name='cooktop',
name='gas_cooktop',
capabilities=_build([
*ignored.IGNORED,
cooktop.COOKTOP_POWER,
@@ -0,0 +1,23 @@
"""Dehumidifier device registry (Samsung TP1X_DA_AC_DHM-class, issue #88).
Shares the DA_AC_ board family with airconditioner.py (power, air filter,
auto-clean, mute-once all use the identical resource shapes), so those three
Capability objects are reused directly rather than duplicated.
"""
from ..capabilities import airconditioner, common, dehumidifier, ignored
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
name='dehumidifier',
capabilities=_build([
*ignored.IGNORED,
*common.UNIVERSAL,
*common.POWER,
dehumidifier.MODE,
dehumidifier.HUMIDITY,
airconditioner.AUTO_CLEAN,
airconditioner.AIR_FILTER,
airconditioner.MUTE_ONCE,
*dehumidifier.COVERAGE,
]),
)
@@ -0,0 +1,30 @@
"""Standalone induction-cooktop device registry (Samsung
TP1X_DA-KS-COOKTOP-class, issue #86) -- the cooktop half of the
range/oven board family (see capabilities/range.py), but with no oven
attached at all.
Reuses range.py's COOKTOP_STATUS/COOKTOP_SPEC/COOKTOP_SAFETY/PROBE_STATUS
wholesale (identical resource shapes to the range combo's cooktop half)
and cooktop.PAIRED_HOOD_STATUS for the Bluetooth-paired range hood some
units pair with. Distinct registry key from cooktop.REGISTRY ('cooktop')
-- that family is the unrelated NA9300K-class gas cooktop (burner state
embedded in /mode/vs/0's options array, a completely different OCF
surface that happens to share the English word "cooktop").
"""
from ..capabilities import common, cooktop as cooktop_caps, ignored
from ..capabilities import range as range_caps
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
name='induction_cooktop',
capabilities=_build([
*ignored.IGNORED,
*common.UNIVERSAL,
*common.POWER,
range_caps.COOKTOP_STATUS,
range_caps.COOKTOP_SPEC,
range_caps.COOKTOP_SAFETY,
range_caps.PROBE_STATUS,
cooktop_caps.PAIRED_HOOD_STATUS,
]),
)
@@ -15,5 +15,6 @@ REGISTRY = DeviceRegistry(
oven.OVEN_DOOR,
oven.OVEN_CONNECTED,
oven.OVEN_SPEC,
oven.OVEN_RECIPE_COOK,
]),
)
@@ -25,5 +25,6 @@ REGISTRY = DeviceRegistry(
range_caps.COOKTOP_STATUS,
range_caps.COOKTOP_SPEC,
range_caps.COOKTOP_SAFETY,
range_caps.COOKTOP_MONITORING,
]),
)
@@ -33,7 +33,7 @@ REGISTRY = DeviceRegistry(
]),
pattern_capabilities=[
fridge.TEMP_CURRENT_GENERIC,
fridge.TEMP_SETPOINT_GENERIC,
fridge.TEMP_SETPOINT,
fridge.ICEMAKER_GENERIC,
fridge.DOOR_GENERIC,
],
@@ -0,0 +1,22 @@
"""Stick-vacuum clean/auto-empty station device registry (issue #131).
See capabilities/vacuum_station.py's module docstring for why this only
covers the station's own dustbag/dustbin/UV-sanitize state and not any
vacuum-body control (suction, battery, cleaning mode) -- the diagnostics
dump this was built from reports none of that.
"""
from ..capabilities import common, ignored, vacuum_station
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
name='vacuum_station',
capabilities=_build([
*ignored.IGNORED,
*common.UNIVERSAL,
*common.POWER,
vacuum_station.DUSTBAG,
vacuum_station.DUSTBAG_USAGE,
vacuum_station.DUSTBIN_SETTING,
vacuum_station.CLEANSTATION_STATUS,
]),
)
@@ -0,0 +1,20 @@
"""Water-purifier device registry (Samsung TP2X_WATERPURIFIER-class, issue #90).
"""
from ..capabilities import common, ignored, water_purifier
from ._base import DeviceRegistry, _build
REGISTRY = DeviceRegistry(
name='water_purifier',
capabilities=_build([
*ignored.IGNORED,
*common.UNIVERSAL,
common.WATER_FILTER,
water_purifier.DISPENSE,
water_purifier.STATUS,
water_purifier.FAVORITE_CAPACITY,
water_purifier.FAVORITE_HOTWATER,
water_purifier.COFFEE,
water_purifier.LOCK,
*water_purifier.COVERAGE,
]),
)
@@ -15,6 +15,13 @@ issue #56's follow-up (five diagnostics dumps captured with the physical unit
set to Auto/Sleep/Low/Medium/High):
Light_On / Light_Off -- a plain on/off flag; MODE below models it as a
real switch, RMW-replacing just that one entry.
NOT the same polarity as the AC family's own
Light_On/Light_Off token on its own /mode/vs/0
(airconditioner._display_light_on) -- that one is
confirmed inverted (Light_Off means the panel is
lit) on live hardware. Same token name, same
resource name, different device type and
opposite meaning -- don't unify them.
Comode_Off -- read 'Off' on *every* one of the five dumps,
including High/Low/Medium/Auto -- confirms this
is NOT the fan-speed selector (ruling out the
@@ -42,25 +49,41 @@ diagnostics snapshot was taken. Exposed read-only pending a confirmed,
stable capture -- see the issue #56 discussion for what's needed.
"""
from ..capability import Capability
from ..entities import BinarySensorDesc, SensorDesc, SwitchDesc
from .common import int_or_none, sensor_item_value
from ..entities import (
BinarySensorDesc, FanDesc, NumberDesc, SelectDesc, SensorDesc, SwitchDesc,
)
from .common import filter_usage_percent, int_or_none, sensor_item_value
from .laundry import bool_option_exists, bool_option_value, option_value, option_write
# Newer TP1X_DA-AC-AIR-class boards (e.g. TP1X_DA-AC-AIR-01031_0000, issue
# #130) report fan modes directly on /mode/vs/0's top-level `modes`/
# `supportedModes` fields (Smart/Max/Mid/WindFree/Sleep) instead of packing
# everything into the options[] array the way the older ARTIK051_TVTL
# family above does -- that older family's /mode/vs/0 has no top-level
# supportedModes at all (see the module docstring's Comode_Off finding).
# Both board generations share the /mode/vs/0 href, so FAN and MODE below
# are mutually exclusive via this presence check rather than colliding.
HREF_MODE = '/mode/vs/0'
def _has_top_level_modes(rep, resources):
return isinstance(rep.get('x.com.samsung.da.supportedModes'), (list, tuple))
_AIR_QUALITY_SENSORS = (
('dust', 'Dust', 'mdi:blur', 'Dust'),
('fine_dust', 'Fine dust', 'mdi:blur', 'FineDust'),
('super_fine_dust', 'Super fine dust', 'mdi:blur', 'SuperFineDust'),
('odor', 'Odor', 'mdi:scent', 'Odor'),
('clean_level', 'Clean level', 'mdi:air-filter', 'CleanLevel'),
('dust', 'mdi:blur', 'Dust'),
('fine_dust', 'mdi:blur', 'FineDust'),
('super_fine_dust', 'mdi:blur', 'SuperFineDust'),
('odor', 'mdi:scent', 'Odor'),
('clean_level', 'mdi:air-filter', 'CleanLevel'),
)
AIR_QUALITY = Capability(
href='/sensors/vs/0',
poll_tier='warm',
entities=tuple(
SensorDesc(key=key, field='x.com.samsung.da.items', name=name, icon=icon,
SensorDesc(key=key, field='x.com.samsung.da.items', icon=icon,
value_fn=lambda items, t=sensor_type: sensor_item_value(items, t))
for key, name, icon, sensor_type in _AIR_QUALITY_SENSORS
for key, icon, sensor_type in _AIR_QUALITY_SENSORS
),
)
@@ -85,7 +108,7 @@ FILTER = Capability(
poll_tier='cold',
entities=(
SensorDesc(key='filter_progress', field='x.com.samsung.da.items',
name='Filter progress', unit='%', state_class='measurement',
unit='%', state_class='measurement',
icon='mdi:air-filter', entity_category='diagnostic',
value_fn=lambda items: int_or_none(
_consumable_state(items, 'FilterProgress'))),
@@ -97,7 +120,7 @@ DEVICE_ACTIVE = Capability(
poll_tier='cold',
entities=(
BinarySensorDesc(key='device_active', field='x.com.samsung.da.deviceActive',
name='Device active', icon='mdi:check-network-outline',
icon='mdi:check-network-outline',
entity_category='diagnostic',
value_fn=lambda v: bool(v)),
),
@@ -110,10 +133,10 @@ AIRFLOW_GENERIC = Capability(
poll_tier='warm',
entities=(
SensorDesc(key='fan_speed_level', field='speed',
name='Fan speed level', icon='mdi:fan',
icon='mdi:fan',
state_class='measurement', entity_category='diagnostic'),
SensorDesc(key='fan_direction', field='direction',
name='Fan direction', icon='mdi:rotate-3d-variant',
icon='mdi:rotate-3d-variant',
entity_category='diagnostic'),
),
)
@@ -124,11 +147,11 @@ AIRFLOW_VS_FALLBACK = Capability(
poll_tier='warm',
entities=(
SensorDesc(key='fan_speed_level', field='x.com.samsung.da.speedLevel',
name='Fan speed level', icon='mdi:fan',
icon='mdi:fan',
state_class='measurement', entity_category='diagnostic',
value_fn=int_or_none),
SensorDesc(key='fan_direction', field='x.com.samsung.da.direction',
name='Fan direction', icon='mdi:rotate-3d-variant',
icon='mdi:rotate-3d-variant',
entity_category='diagnostic'),
),
)
@@ -149,26 +172,216 @@ def _light_write(payload, rep, href=None):
MODE = Capability(
href='/mode/vs/0',
poll_tier='warm',
match_fn=lambda rep, resources: not _has_top_level_modes(rep, resources),
entities=(
SwitchDesc(key='display_light', name='Display light', icon='mdi:led-on',
SwitchDesc(key='display_light', icon='mdi:led-on',
entity_category='config',
rep_fn=bool_option_value('Light'),
exists_fn=bool_option_exists('Light'),
write_fn=_light_write),
# Read-only -- confirmed NOT the fan-speed selector (see module
# docstring), actual purpose still unconfirmed.
SensorDesc(key='operating_mode', name='Operating mode', icon='mdi:fan',
SensorDesc(key='operating_mode', icon='mdi:fan',
entity_category='diagnostic',
rep_fn=lambda rep: option_value(rep.get('x.com.samsung.da.options'), 'Comode'),
exists_fn=bool_option_exists('Comode')),
),
)
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'})
if kind == 'mode':
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': [value]}
return None
def _first_fan_mode(rep):
"""Representative scalar for the fan entity in the flattened state
(golden/regression), mirroring airconditioner.py's own _first_mode --
the real entity computes its state from live coordinator reads."""
modes = rep.get('x.com.samsung.da.modes')
if isinstance(modes, (list, tuple)):
return modes[0] if modes else None
return modes
# Named preset modes (Smart/Max/Mid/WindFree/Sleep), not an ordered
# percentage -- WindFree/Smart/Sleep are named behaviors, not
# "faster/slower" positions relative to Max/Mid, so fan.py's entity for
# this only exposes PRESET_MODE, matching how the AC family's own named
# convenient modes are modeled as a preset rather than a speed number.
FAN = Capability(
href=HREF_MODE,
poll_tier='warm',
match_fn=_has_top_level_modes,
entities=(
FanDesc(key='fan', translation_key='air_purifier_fan',
rep_fn=_first_fan_mode, write_fn=_fan_write),
),
)
# ---------------------------------------------------------------------------
# TP1X_DA-AC-AIR-class additions (issue #130). This board reports several
# resources the older ARTIK051_TVTL family never did.
# ---------------------------------------------------------------------------
# Screen/indicator-panel on/off -- distinct from LIGHT below (ambient mood
# light): both report the same {mode, supportedModes: [On, Off]} shape on
# separate hrefs on this dump, so they're two independent physical controls,
# not a duplicate encoding of one.
DISPLAY = Capability(
href='/display/vs/0',
poll_tier='cold',
entities=(
SwitchDesc(key='display', field='mode',
icon='mdi:monitor',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
['display', 'vs', '0'],
{'mode': 'On' if p == 'On' else 'Off'})),
),
)
# Same filterUsage/filterCapacity/filterStatus shape as the AC family's own
# AIR_FILTER (airconditioner.py) -- confirmed normal/wash/replace values not
# seen on this one dump, so the option list there is reused as-is rather
# than re-deriving it from a single sample.
HEPA_FILTER = Capability(
href='/filter/hepafilter/vs/0',
poll_tier='cold',
entities=(
SensorDesc(key='hepa_filter_usage', rep_fn=filter_usage_percent,
unit='%', state_class='measurement',
icon='mdi:air-filter', entity_category='diagnostic'),
SensorDesc(key='hepa_filter_status', field='x.com.samsung.da.filterStatus',
device_class='enum',
options=('normal', 'wash', 'replace'),
translation_key='filter_status',
icon='mdi:air-filter', entity_category='diagnostic',
value_fn=lambda v: v.lower() if isinstance(v, str) else v),
),
)
# Physical panel/cover status -- meaning of the one value seen ('Close') is
# plausible (the HEPA-filter access cover) but unconfirmed, and no
# supportedStatus list is present to check against -- exposed as a plain
# diagnostic sensor rather than an asserted binary_sensor polarity.
PANEL_STATUS = Capability(
href='/panel/vs/0',
poll_tier='cold',
entities=(
SensorDesc(key='panel_status', field='status',
icon='mdi:archive-outline', entity_category='diagnostic'),
),
)
# Pet-care filter mode -- a plain On/Off field with no vendor prefix, same
# convention as airconditioner.MUTE_ONCE.
PET_FILTER_ACTIVATION = Capability(
href='/petfilteractivation/vs/0',
poll_tier='cold',
entities=(
SwitchDesc(key='pet_filter_activation', field='status',
icon='mdi:paw',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
['petfilteractivation', 'vs', '0'],
{'status': 'On' if p == 'On' else 'Off'})),
),
)
# Sound mode/volume shapes look like laundry.py's SOUND_MODE/SOUND_VOLUME at
# a glance, but this board's actual values differ (supportedModes here is
# ['mute', 'buzzer'], not laundry's hardcoded voice/tone/mute; volume range
# is 0-3, not laundry's fixed 0-15) -- reusing those would either reject a
# valid write ('buzzer') or expose the wrong number range, so these are
# separate descriptors reading the live supported values instead of a
# hardcoded table.
SOUND_MODE = Capability(
href='/settings/sound/mode/vs/0',
poll_tier='cold',
entities=(
# Distinct translation_key from laundry.SOUND_MODE's shared
# 'sound_mode' catalog entry -- that one's state table is
# {voice, tone, mute}, but this board's supportedModes is
# {mute, buzzer}. Sharing the key would leave 'buzzer' unlabelled
# (falls through to the raw code) since the catalogs don't overlap.
SelectDesc(key='sound_mode', translation_key='air_purifier_sound_mode',
field='mode',
icon='mdi:volume-high',
entity_category='config',
options_field='supportedModes',
write_fn=lambda p, rep, href=None: (
['settings', 'sound', 'mode', 'vs', '0'], {'mode': p})),
),
)
# Read-only descriptor of which sound output the unit has -- only one value
# seen, no alternatives to select between.
SOUND_OUTPUT = Capability(
href='/settings/sound/output/vs/0',
poll_tier='cold',
entities=(
SensorDesc(key='sound_output', field='deviceType',
icon='mdi:volume-high', entity_category='diagnostic'),
),
)
SOUND_VOLUME = Capability(
href='/settings/sound/volume/vs/0',
poll_tier='cold',
entities=(
NumberDesc(key='sound_volume', field='level',
icon='mdi:volume-medium',
entity_category='config',
native_min_fn=lambda rep: int_or_none(rep.get('minLevel')) or 0,
native_max_fn=lambda rep: int_or_none(rep.get('maxLevel')) or 0,
step_fn=lambda rep: int_or_none(rep.get('resolution')) or 1,
value_fn=int_or_none,
write_fn=lambda p, rep, href=None: (
['settings', 'sound', 'volume', 'vs', '0'],
{'level': str(int(p))})),
),
)
# /humidity/0 and /humidity/vs/0 are empty {} on both dumps this family has
# been verified against -- covered here (not globally, per ignored.py's
# module docstring) since those hrefs collide with fridge/AC schemas
# elsewhere. Same two hrefs and reasoning as airconditioner.py's _AC_IGNORED.
#
# The next six hrefs (issue #130, TP1X_DA-AC-AIR board) are the exact same
# resources, same shapes, same reasoning as airconditioner.py's
# _AC_IGNORED on the shared DA-AC- board family -- duplicated here rather
# than promoted to the global ignored.py list, since that would require
# also removing them from _AC_IGNORED in the same change (a global entry
# colliding with a family-local bare Capability on the same href raises in
# _build()); left as a possible follow-up DRY cleanup.
COVERAGE = [
Capability(href='/humidity/0'),
Capability(href='/humidity/vs/0'),
Capability(href='/airlevelcheck/vs/0'), # periodic air-quality sensing scheduler plumbing
Capability(href='/availablecontrolsets/vs/0'), # opaque hex-encoded control-set bitmap
Capability(href='/da/softreset/vs/0'), # soft-reset trigger plumbing
Capability(href='/keepnormalstate/vs/0'), # internal keep-normal flag
Capability(href='/personality/presence/vs/0'), # presence-personalization plumbing (empty here)
Capability(href='/reserverulesets/vs/0'), # opaque hex-encoded schedule reservation blob
# Do-not-disturb/auto-sleep schedule (visible/startTime/endTime/
# useTimeSetting/functionState) -- every field reads its inert default
# on the only dump seen (times both '00:00:00', useTimeSetting/
# 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'),
]
@@ -16,6 +16,8 @@ by_type registry.
"""
from ..capability import Capability
from ..entities import BinarySensorDesc, ClimateDesc, SensorDesc, SwitchDesc
from .common import filter_usage_percent, normalize_temp_unit
from .laundry import option_write
# ---------------------------------------------------------------------------
# Canonical AC resource hrefs. The climate entity (climate.py) binds the
@@ -33,13 +35,19 @@ HREF_TEMP_DESIRED = '/temperature/desired/0' # target_temperature (write ta
HREF_TEMP_CONTROL = '/temperature/control/vs/0' # target_temperature_step
HREF_WIND_STRENGTH = '/wind/strength/vs/0' # fan_mode
HREF_WIND_DIRECTION = '/wind/direction/vs/0' # swing_mode
# Newer boards (issue #126, TP1X_DA-AC-RAC-01011 WindFree variant) report no
# HREF_WIND_DIRECTION at all and instead carry a 2-axis oscillation resource
# (separate vertical/horizontal Swing|Fix toggles, each with its own angle).
# climate.py falls back to this when HREF_WIND_DIRECTION is absent, the same
# duality pattern as the OCF/vendor temperature channel below.
HREF_WIND_OSCILLATION = '/wind/oscillation/vs/0' # swing_mode fallback
HREF_CONVENIENT = '/mode/convenient/vs/0' # preset_mode
HREF_TEMPS_VS = '/temperatures/vs/0' # vendor temp fallback (items[] array)
CLIMATE_CONSUMED_HREFS = [
HREF_POWER, HREF_POWER_VS, HREF_TEMP_CURRENT, HREF_TEMP_DESIRED,
HREF_TEMP_CONTROL, HREF_TEMPS_VS, HREF_WIND_STRENGTH, HREF_WIND_DIRECTION,
HREF_CONVENIENT,
HREF_WIND_OSCILLATION, HREF_CONVENIENT,
]
@@ -50,16 +58,23 @@ def _num(v):
return None
def _filter_usage_percent(rep):
"""Filter usage as a percentage of rated capacity. The device reports
`filterUsage` as a raw count in `filterCapacityUnit` (Hours here, e.g.
100 of a 500 capacity), so a plain value with a '%' unit would be wrong --
normalize to used/capacity. Returns None when capacity is missing/zero."""
used = _num(rep.get('x.com.samsung.da.filterUsage'))
cap = _num(rep.get('x.com.samsung.da.filterCapacity'))
if used is None or not cap:
return None
return round(used / cap * 100)
def _temps_vs_item(rep):
"""First item of the vendor `/temperatures/vs/0` items[] array -- the
Tizen Lite board's only current-temperature source (see climate.py's
identical helper; duplicated rather than imported to avoid a
capabilities<->platform import cycle)."""
items = rep.get('x.com.samsung.da.items')
if isinstance(items, (list, tuple)) and items and isinstance(items[0], dict):
return items[0]
return {}
def _temps_vs_current(rep):
return _num(_temps_vs_item(rep).get('x.com.samsung.da.current'))
def _temps_vs_unit(rep):
return normalize_temp_unit(_temps_vs_item(rep).get('x.com.samsung.da.unit'), '°C')
def _first_mode(rep):
@@ -71,25 +86,100 @@ def _first_mode(rep):
return modes
def _mode_options(rep):
opts = rep.get('x.com.samsung.da.options')
return opts if isinstance(opts, (list, tuple)) else ()
def _has_display_light_option(rep, resources):
"""True when the panel light state is carried inside /mode/vs/0's options
blob (a `Light_On`/`Light_Off` token) rather than a dedicated /light/vs/0
switch. The two encodings are mutually exclusive across observed boards:
models exposing the /light/vs/0 switch (bound by DISPLAY_LIGHT below)
carry no Light_* option, so this entity only materialises on the boards
that would otherwise have no display-light entity at all."""
return any(isinstance(o, str) and o.startswith('Light_')
for o in _mode_options(rep))
def _display_light_on(rep):
"""Panel display light state from /mode/vs/0's options blob. The token is
INVERTED relative to its name -- confirmed by a live toggle test: with the
panel lit the option reads `Light_Off`, and with it dark it reads
`Light_On` (the flag really encodes "night/display-off mode active"). So
`Light_Off` -> light on, `Light_On` -> light off. Read-only from here; the
write below uses the device's own single-token merge (see option_write)."""
for o in _mode_options(rep):
if isinstance(o, str) and o.startswith('Light_'):
return o == 'Light_Off'
return None
def _display_light_write(payload, rep, href=None):
"""Toggle the panel light via a single-token /mode/vs/0 options write
('SingleCommand_1' is advertised in this family's /configuration/vs/0
airconOptionList; option_write's one-token merge is the same mechanism
air_purifier.py uses for its own Light switch). Polarity is inverted (see
_display_light_on): switching the lamp ON writes 'Light_Off', OFF writes
'Light_On'."""
token = 'Off' if payload == 'On' else 'On'
return (['mode', 'vs', '0'],
{'x.com.samsung.da.options': option_write('Light', token)})
def _climate_write(payload, rep, href=None):
"""Map a (kind, value) command from the climate platform to the
(path_segs, body) for that one sub-write. `value` is already the raw device
code (the platform maps HA<->device). async_send_command POSTs to path_segs,
so a single desc drives writes across power/mode/temperature/wind resources.
Read-modify-write safe: each write sends only its own field, leaving the
resource's other fields (e.g. /mode/vs/0's opaque `options` blob) untouched.
so a single desc drives writes across the power/mode/temperature/wind
resources.
Power goes to the vendor `/power/vs/0` (the OCF `/power/0` is absent on
most boards and a non-authoritative mirror where present -- vendor works
on every board). Temperature is board-dependent, so the platform picks the
channel and sends `temperature_ocf` (-> OCF `/temperature/desired/0`,
boards with the full OCF current+desired pair) or `temperature` (-> vendor
`/temperatures/vs/0`, boards without it). Mode/fan/swing/preset are always
the vendor `/x/vs/0` resources.
Each write sends only its own field(s), leaving the resource's other
fields (e.g. /mode/vs/0's opaque `options` blob, or the vendor temperature
item's current/minimum/maximum/unit) untouched -- the device merges the
rest itself, same contract as the options[] array (see
common.merge_items_field / merge_options_field, which keep the
coordinator's optimistic cache complete for the settle window instead of
this write echoing those fields back).
"""
kind, value = payload
if kind == 'power':
return (['power', '0'], {'value': bool(value)})
return (['power', 'vs', '0'],
{'x.com.samsung.da.power': 'On' if value else 'Off'})
if kind == 'mode':
return (['mode', 'vs', '0'], {'x.com.samsung.da.modes': [value]})
if kind == 'temperature_ocf':
return (['temperature', 'desired', '0'],
{'temperature': int(round(float(value)))})
if kind == 'temperature':
return (['temperature', 'desired', '0'], {'temperature': int(round(float(value)))})
# Vendor items[] array; only one item observed on every AC dump, id
# '0'. See the docstring above for why this doesn't echo current/
# minimum/maximum/unit back at the unit.
return (['temperatures', 'vs', '0'],
{'x.com.samsung.da.items': [
{'x.com.samsung.da.id': '0',
'x.com.samsung.da.desired': str(int(round(float(value))))}]})
if kind == 'fan':
return (['wind', 'strength', 'vs', '0'], {'x.com.samsung.da.modes': value})
if kind == 'swing':
return (['wind', 'direction', 'vs', '0'], {'x.com.samsung.da.modes': value})
if kind == 'oscillation':
# value is the HA swing_mode string ('off'/'vertical'/'horizontal'/
# 'both'); both axes are independent Swing|Fix toggles on this
# resource, so one HA value maps to a pair of fields written
# together (see climate.py's oscillation fallback).
return (['wind', 'oscillation', 'vs', '0'], {
'vertical': 'Swing' if value in ('vertical', 'both') else 'Fix',
'horizontal': 'Swing' if value in ('horizontal', 'both') else 'Fix',
})
if kind == 'preset':
return (['mode', 'convenient', 'vs', '0'], {'x.com.samsung.da.modes': value})
return None
@@ -101,6 +191,19 @@ CLIMATE = Capability(
entities=(
ClimateDesc(key='climate', translation_key='airconditioner',
rep_fn=_first_mode, write_fn=_climate_write),
# Display (panel) light switch, only on boards that encode it in
# /mode/vs/0's options instead of a /light/vs/0 switch (see
# _has_display_light_option). Shares the /mode/vs/0 href with the
# climate entity above -- same Capability, so no multi-cap
# discriminator is needed. /mode/vs/0 is OBSERVE-subscribed, so state
# updates on push; writes go through the single-token merge in
# _display_light_write. Shares the switch.display_light translation
# with DISPLAY_LIGHT (the /light/vs/0 switch on other boards) --
# mutually exclusive per href, so only one ever binds for a given unit.
SwitchDesc(key='display_light', rep_fn=_display_light_on,
exists_fn=_has_display_light_option,
write_fn=_display_light_write,
icon='mdi:led-on', entity_category='config'),
),
)
@@ -109,7 +212,7 @@ AIR_PURIFY = Capability(
poll_tier='warm',
entities=(
SwitchDesc(key='air_purify', field='x.com.samsung.da.modes',
name='Air purification', icon='mdi:air-purifier',
icon='mdi:air-purifier',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
@@ -123,7 +226,7 @@ AUTO_CLEAN = Capability(
poll_tier='cold',
entities=(
SwitchDesc(key='auto_clean', field='x.com.samsung.da.settingStatus',
name='Auto clean', icon='mdi:spray-bottle',
icon='mdi:spray-bottle',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
@@ -136,13 +239,13 @@ AIR_FILTER = Capability(
href='/filter/airdustfilter/vs/0',
poll_tier='cold',
entities=(
SensorDesc(key='air_filter_usage', rep_fn=_filter_usage_percent,
name='Filter usage', unit='%', state_class='measurement',
SensorDesc(key='air_filter_usage', rep_fn=filter_usage_percent,
unit='%', state_class='measurement',
icon='mdi:air-filter', entity_category='diagnostic'),
SensorDesc(key='air_filter_status', field='x.com.samsung.da.filterStatus',
name='Filter status', device_class='enum',
device_class='enum',
options=('normal', 'wash', 'replace'),
translation_key='air_filter_status',
translation_key='filter_status',
icon='mdi:air-filter', entity_category='diagnostic',
value_fn=lambda v: v.lower() if isinstance(v, str) else v),
),
@@ -153,7 +256,7 @@ DISPLAY_LIGHT = Capability(
poll_tier='cold',
entities=(
SwitchDesc(key='display_light', field='mode',
name='Display light', icon='mdi:led-on',
icon='mdi:led-on',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
@@ -170,7 +273,7 @@ MUTE_ONCE = Capability(
poll_tier='warm',
entities=(
SwitchDesc(key='mute_once', field='muteonce',
name='Mute once', icon='mdi:volume-mute',
icon='mdi:volume-mute',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
@@ -190,15 +293,82 @@ CURRENT_LIMIT = Capability(
poll_tier='cold',
entities=(
BinarySensorDesc(key='current_limit_enabled', field='operation',
name='Current limit enabled', icon='mdi:current-ac',
icon='mdi:current-ac',
entity_category='diagnostic',
value_fn=lambda v: v == 'On'),
SensorDesc(key='current_limit_level', field='modes',
name='Current limit level', icon='mdi:current-ac',
icon='mdi:current-ac',
entity_category='diagnostic'),
),
)
# Overload-response setting (issue #126, TP1X_DA-AC-RAC-01011 WindFree
# variant): `operation` toggles the feature and `mode` picks 'Alarm' vs
# 'PowerSaving' out of `supportedModes`, with a `savingTime` duration
# ('20'/'40'/'60' minutes) alongside. Plausible shape (compressor-overload
# alarm vs. automatic power throttling), but nothing in the dump confirms
# the exact behavioral difference between the two modes or whether writing
# 'operation' is safe on live HVAC hardware -- exposed read-only per the
# 'don't guess' rule, same precedent as CURRENT_LIMIT above.
ANOMALY_LOAD = Capability(
href='/anomalyload/vs/0',
poll_tier='cold',
entities=(
BinarySensorDesc(key='overload_protection_active', field='operation',
icon='mdi:flash-alert',
entity_category='diagnostic',
value_fn=lambda v: v == 'On'),
SensorDesc(key='overload_protection_mode', field='mode',
device_class='enum',
options=('alarm', 'powersaving'),
translation_key='overload_protection_mode',
icon='mdi:flash-alert', entity_category='diagnostic',
value_fn=lambda v: v.lower() if isinstance(v, str) else v),
),
)
# The climate entity already surfaces current_temperature as a card
# attribute, but that's not enough for history graphs/automations/
# statistics -- issue #75 asked for a standalone sensor. Same OCF-standard-
# with-vendor-fallback shape as common.py's POWER_GENERIC/POWER_VS_FALLBACK
# pair: both share key='current_temperature_c' so only one ever binds
# (match_fn gates the vendor one off when the OCF resource is present).
CURRENT_TEMPERATURE = Capability(
href=HREF_TEMP_CURRENT,
poll_tier='warm',
entities=(
SensorDesc(key='current_temperature_c', field='temperature',
device_class='temperature', state_class='measurement',
unit_fn=lambda rep: normalize_temp_unit(rep.get('units'), '°C')),
),
)
CURRENT_TEMPERATURE_VS = Capability(
href=HREF_TEMPS_VS,
poll_tier='warm',
match_fn=lambda rep, resources: HREF_TEMP_CURRENT not in resources,
entities=(
SensorDesc(key='current_temperature_c', rep_fn=_temps_vs_current,
device_class='temperature', state_class='measurement',
unit_fn=_temps_vs_unit),
),
)
# Only the vendor resource's `fivepercentHumidity` (current reading, rounded
# to the nearest 5%) has live data on the issue #75 dump -- its `humidity`
# field, and the OCF-standard /humidity/0 resource entirely, both read a
# stuck "0" there and stay ignored per the 'don't guess' rule (see
# _AC_IGNORED below).
HUMIDITY = Capability(
href='/humidity/vs/0',
poll_tier='warm',
entities=(
SensorDesc(key='humidity', field='x.com.samsung.da.fivepercentHumidity',
device_class='humidity', state_class='measurement', unit='%',
value_fn=_num),
),
)
# ---------------------------------------------------------------------------
# AC-scoped coverage: the CLIMATE_CONSUMED_HREFS above (read by the climate
# entity) plus vendor duplicates / all-zero-ambiguous / plumbing resources.
@@ -221,8 +391,10 @@ _AC_IGNORED = [
# All-zero and ambiguously encoded on this model (2-value arrays); the
# 'don't guess' rule -- leave unmodeled rather than invent entities.
'/sensors/vs/0',
# Stuck at "0" on every dump seen -- HUMIDITY above reads the vendor
# resource's usable fivepercentHumidity field instead; this OCF-standard
# one has no corresponding live value confirmed yet.
'/humidity/0',
'/humidity/vs/0',
# Presence-personalization plumbing (empty item list here).
'/personality/presence/vs/0',
# --- TP1X/TP2X-class housekeeping / opaque blobs. These carry no
@@ -240,7 +412,14 @@ _AC_IGNORED = [
'/mds/absencemonitoring/vs/0', # motion-detection sensor plumbing (empty here)
'/mds/absencestate/vs/0', # motion-detection state (empty here)
'/remotedatacontrol/vs/0', # remote data-control session status
'/remotedeviceinfo/vs/0', # remote paired-device id list (empty didList here)
'/remotetemperature/vs/0', # external temp-sensor feed (unset on this unit)
# Manual airflow-step position (supportedModes Off/80/60/40/Power).
# Overlaps the /wind/direction swing control already on the climate card,
# and the meaning of the numeric steps vs. 'Power' isn't documented in the
# dump -- ignored per the 'don't guess' rule rather than modeled as a
# select whose write could confuse live HVAC hardware.
'/stepcontrol/vs/0',
'/reserverulesets/vs/0', # opaque hex-encoded schedule reservation blob
'/welcome/temperature/vs/0', # welcome-cooling plumbing
# System-AC-only (multi-indoor-unit commercial installs, e.g.
@@ -251,8 +430,11 @@ _AC_IGNORED = [
]
# Built as bare no-entity caps; folded into the AC registry (not global).
# HREF_TEMP_CURRENT and HREF_TEMPS_VS are excluded here -- CURRENT_TEMPERATURE
# / CURRENT_TEMPERATURE_VS above already cover those two with real entities.
COVERAGE = [
Capability(href=h, poll_tier='warm') for h in CLIMATE_CONSUMED_HREFS
if h not in (HREF_TEMP_CURRENT, HREF_TEMPS_VS)
] + [
Capability(href=h) for h in _AC_IGNORED
]
@@ -10,6 +10,8 @@ against live device dumps:
/water/consumption/vs/0 -> x.com.samsung.da.cumulativeWater
/filter/waterfilter/vs/0 -> x.com.samsung.da.filterUsage / filterStatus
"""
from datetime import datetime, timezone
from ..capability import Capability
from ..entities import (
BinarySensorDesc, ButtonDesc, SelectDesc, SensorDesc, SwitchDesc,
@@ -40,6 +42,35 @@ def wh_to_kwh(v):
return round(n / 1000.0, 2) if n is not None else None
def parse_iso_utc(raw):
"""ISO datetime defaulting to UTC when the string carries no timezone
of its own (this integration's convention for other bare ISO datetime
fields -- see washer.py's drum-clean-log comment). A few boards do
ship a 'Z'/offset suffix (fromisoformat parses that natively since
Python 3.11) -- only fill in UTC when parsing left the result naive,
rather than unconditionally overwriting whatever offset was parsed."""
if not raw:
return None
try:
dt = datetime.fromisoformat(raw)
except ValueError:
return None
return dt if dt.tzinfo is not None else dt.replace(tzinfo=timezone.utc)
def filter_usage_percent(rep):
"""Filter usage as a percentage of rated capacity. Several families
(AC, air purifier) report `filterUsage` as a raw count in
`filterCapacityUnit` (Hours, e.g. 100 of a 500 capacity), so a plain
value with a '%' unit would be wrong -- normalize to used/capacity.
Returns None when capacity is missing/zero."""
used = _num(rep.get('x.com.samsung.da.filterUsage'))
cap = _num(rep.get('x.com.samsung.da.filterCapacity'))
if used is None or not cap:
return None
return round(used / cap * 100)
def normalize_temp_unit(raw, default='°F'):
"""'C'/'Celsius' -> '°C', 'F'/'Fahrenheit' -> '°F'. Falls back to
`default` for any other/missing value. Shared by fridge.py and oven.py,
@@ -59,9 +90,21 @@ def _ml_to_l(v):
def _active_alarm_codes(items):
"""Join active alarm codes; skip retained rows Samsung leaves as Deleted.
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.
"""
if not items or not isinstance(items, list):
return 'none'
codes = [i.get('x.com.samsung.da.code') for i in items if i.get('x.com.samsung.da.code')]
codes = [
i.get('x.com.samsung.da.code')
for i in items
if i.get('x.com.samsung.da.code')
and str(i.get('x.com.samsung.da.state', '')).lower() != 'deleted'
]
return ', '.join(codes) if codes else 'none'
@@ -92,6 +135,80 @@ def merge_options_field(cached, new_tokens):
return merged
def merge_items_field(cached, new_items):
"""Merge a partial x.com.samsung.da.items[]-style write (matched by
x.com.samsung.da.id) into a cached items array -- the read-side
counterpart of merge_options_field above, for the items[] shape instead
of the packed options[] shape.
Confirmed on hardware that a write only needs to carry the array item
with the changed id plus the field(s) being changed; the device merges
the rest itself (same fact as the options[] case, different array --
see airconditioner._climate_write's vendor temperature write). Fields
within the matched item are merged, not replaced outright, so a
setpoint-only write doesn't wipe current/minimum/maximum/unit from the
optimistic cache entry for the settle window. An id with no match in
`cached` is appended."""
merged = [dict(i) if isinstance(i, dict) else i for i in (cached or [])]
for new_item in new_items or ():
if not isinstance(new_item, dict):
continue
item_id = new_item.get('x.com.samsung.da.id')
for i, existing in enumerate(merged):
if isinstance(existing, dict) and existing.get('x.com.samsung.da.id') == item_id:
merged[i] = {**existing, **new_item}
break
else:
merged.append(new_item)
return merged
# /wm/setinfo/vs/0 -- laundry-family firmware capability flags. Present on
# washers, dryers, and dishwashers; absent on fridge/oven/AC. Static for the
# life of a given board, so reading them from the /device/0 seed (no dedicated
# poll_tier) is enough.
_SETINFO_HREF = '/wm/setinfo/vs/0'
_POWER_ON_OFF_FIELD = 'x.com.samsung.da.isModelSettingPowerOnOff'
_WITHOUT_SC_FIELD = 'x.com.samsung.da.isModelSettingWithoutSC'
def model_allows_power_on_off(resources: dict) -> bool:
"""True unless firmware explicitly declares remote power on/off unsupported.
`/wm/setinfo/vs/0`.`isModelSettingPowerOnOff` is `"false"` on many laundry
boards (washers/dryers): `/power/0` and `/power/vs/0` still report state,
but CoAP writes are ignored. Absent setinfo (non-laundry families) keeps
the writable switch -- current behavior.
"""
setinfo = resources.get(_SETINFO_HREF)
if setinfo is None:
return True
flag = setinfo.get(_POWER_ON_OFF_FIELD)
if flag is None:
return True
return str(flag).lower() != 'false'
def model_setting_without_sc(resources: dict) -> bool:
"""True when firmware declares settings writable without Smart Control.
`/wm/setinfo/vs/0`.`isModelSettingWithoutSC` is `"true"` on washers/dryers
that accept temperature/spin/cycle-option writes while remote control is
off. Cycle start/pause/stop still need Smart Control on those boards --
the flag name is settings-specific, not a blanket remote-control bypass.
"""
setinfo = resources.get(_SETINFO_HREF) or {}
return str(setinfo.get(_WITHOUT_SC_FIELD, '')).lower() == 'true'
def _power_switch_exists(rep, resources):
return model_allows_power_on_off(resources)
def _power_sensor_exists(rep, resources):
return not model_allows_power_on_off(resources)
def sensor_item_value(items, sensor_type, index=0):
"""Pull one reading out of a `/sensors/vs/0`-style items[] list -- each
item is `{type, value: [...]}`; `index` picks which slot of a possibly
@@ -128,11 +245,17 @@ def sensor_item_value(items, sensor_type, index=0):
POWER_GENERIC = Capability(
href='/power/0',
entities=(
# Writable when firmware allows remote power; otherwise a read-only
# binary_sensor with the same key keeps HA state without a dead switch.
SwitchDesc(key='power_switch', field='value',
name='Power',
value_fn=lambda v: bool(v),
exists_fn=_power_switch_exists,
write_fn=lambda p, rep, href=None: (
['power', '0'], {'value': p == 'On'})),
BinarySensorDesc(key='power_switch', field='value',
device_class='power',
value_fn=lambda v: bool(v),
exists_fn=_power_sensor_exists),
),
)
@@ -141,11 +264,15 @@ POWER_VS_FALLBACK = Capability(
match_fn=lambda rep, resources: '/power/0' not in resources,
entities=(
SwitchDesc(key='power_switch', field='x.com.samsung.da.power',
name='Power',
value_fn=lambda v: v == 'On',
exists_fn=_power_switch_exists,
write_fn=lambda p, rep, href=None: (
['power', 'vs', '0'],
{'x.com.samsung.da.power': 'On' if p == 'On' else 'Off'})),
BinarySensorDesc(key='power_switch', field='x.com.samsung.da.power',
device_class='power',
value_fn=lambda v: v == 'On',
exists_fn=_power_sensor_exists),
),
)
@@ -153,7 +280,7 @@ KIDS_LOCK_GENERIC = Capability(
href='/kidslock/0',
entities=(
SwitchDesc(key='child_lock', field='value',
name='Child lock', device_class='lock',
device_class='lock',
value_fn=lambda v: bool(v),
write_fn=lambda p, rep, href=None: (
['kidslock', '0'], {'value': p == 'On'})),
@@ -165,7 +292,7 @@ KIDS_LOCK_VS_FALLBACK = Capability(
match_fn=lambda rep, resources: '/kidslock/0' not in resources,
entities=(
SwitchDesc(key='child_lock', field='x.com.samsung.da.kidsLock',
name='Child lock', device_class='lock',
device_class='lock',
value_fn=lambda v: v != 'Ready',
write_fn=lambda p, rep, href=None: (
['kidslock', 'vs', '0'],
@@ -194,12 +321,26 @@ def remote_control_enabled(resources: dict) -> bool:
return True
def remote_control_required_for_write(resources: dict, bound_href: str) -> bool:
"""Whether a write to bound_href should be gated on Smart Control.
When isModelSettingWithoutSC is true, laundry firmware accepts settings
writes (wash temp, spin, course options, buzzer, ...) with remote
control off, but cycle start/pause/stop on /operational/state still
need Smart Control. Absent that flag, keep the historical blanket gate.
"""
if not model_setting_without_sc(resources):
return True
href = bound_href or ''
return href.startswith('/operational/state')
REMOTE_CONTROL_GENERIC = Capability(
href='/remotectrl/0',
poll_tier='warm',
entities=(
BinarySensorDesc(key='remote_control', field='value',
name='Smart Control', device_class='connectivity',
device_class='connectivity',
value_fn=lambda v: bool(v)),
),
)
@@ -211,7 +352,7 @@ REMOTE_CONTROL_VS_FALLBACK = Capability(
entities=(
BinarySensorDesc(key='remote_control',
field='x.com.samsung.da.remoteControlEnabled',
name='Smart Control', device_class='connectivity',
device_class='connectivity',
value_fn=lambda v: str(v).lower() == 'true'),
),
)
@@ -221,7 +362,7 @@ ALARMS = Capability(
poll_tier='hot',
entities=(
SensorDesc(key='alarm_code', field='x.com.samsung.da.items',
name='Alarm code', icon='mdi:alert',
icon='mdi:alert',
entity_category='diagnostic', value_fn=_active_alarm_codes),
),
)
@@ -244,13 +385,13 @@ ENERGY_METER = Capability(
# entity when /device/0 returns a not-yet-fetched stub. On a populated
# rep, hide power only for the dead sentinel or an absent field.
SensorDesc(key='power_watts', field='x.com.samsung.da.instantaneousPower',
name='Power', device_class='power', state_class='measurement',
device_class='power', state_class='measurement',
unit='W', value_fn=clamp_power,
exists_fn=lambda rep, resources: not rep or (
rep.get('x.com.samsung.da.instantaneousPower')
not in (None, _DEAD_INSTANTANEOUS_POWER))),
SensorDesc(key='energy_kwh', field='x.com.samsung.da.cumulativePower',
name='Energy', device_class='energy',
device_class='energy',
state_class='total_increasing', unit='kWh', value_fn=wh_to_kwh,
exists_fn=lambda rep, resources: (
not rep or 'x.com.samsung.da.cumulativePower' in rep)),
@@ -261,7 +402,7 @@ ENERGY_METER = Capability(
# energy_kwh above -- without it, an exists_fn permanently drops the
# entity if setup happens to land on a not-yet-fetched stub.
SensorDesc(key='power_energy_kwh', field='x.com.samsung.da.cumulativeConsumption',
name='Power energy', device_class='energy',
device_class='energy',
state_class='total_increasing', unit='kWh', value_fn=wh_to_kwh,
exists_fn=lambda rep, resources: (
not rep or 'x.com.samsung.da.cumulativeConsumption' in rep)),
@@ -269,7 +410,7 @@ ENERGY_METER = Capability(
# baseline -- present on some models (e.g. TP1X_REF_21K, issue #21/
# #27) and absent on others (issue #20/#26), unlike cumulativePower.
SensorDesc(key='energy_saved_kwh', field='x.com.samsung.da.cumulativeSavedPower',
name='Energy saved', device_class='energy',
device_class='energy',
state_class='total_increasing', unit='kWh', value_fn=wh_to_kwh,
exists_fn=lambda rep, resources: (
not rep or 'x.com.samsung.da.cumulativeSavedPower' in rep)),
@@ -277,12 +418,12 @@ ENERGY_METER = Capability(
# in-progress current month. Not ever-increasing (each resets at
# month boundary), so no state_class.
SensorDesc(key='energy_last_month_kwh', field='x.com.samsung.da.monthlyConsumption',
name='Energy (last month)', device_class='energy',
device_class='energy',
unit='kWh', value_fn=wh_to_kwh,
exists_fn=lambda rep, resources: (
not rep or 'x.com.samsung.da.monthlyConsumption' in rep)),
SensorDesc(key='energy_this_month_kwh', field='x.com.samsung.da.thismonthlyConsumption',
name='Energy (this month)', device_class='energy',
device_class='energy',
unit='kWh', value_fn=wh_to_kwh,
exists_fn=lambda rep, resources: (
not rep or 'x.com.samsung.da.thismonthlyConsumption' in rep)),
@@ -293,7 +434,7 @@ WATER_METER = Capability(
href='/water/consumption/vs/0',
entities=(
SensorDesc(key='water_liters', field='x.com.samsung.da.cumulativeWater',
name='Water consumption', device_class='water',
device_class='water',
state_class='total_increasing', unit='L', icon='mdi:water',
value_fn=_ml_to_l),
),
@@ -304,10 +445,14 @@ WATER_FILTER = Capability(
match_fn=lambda rep, _: rep.get('x.com.samsung.da.filterStatus', '').lower() != 'notused',
entities=(
SensorDesc(key='filter_usage', field='x.com.samsung.da.filterUsage',
name='Filter usage', unit='%', state_class='measurement',
unit='%', state_class='measurement',
icon='mdi:filter'),
SensorDesc(key='filter_status', field='x.com.samsung.da.filterStatus',
name='Filter status', icon='mdi:filter-check'),
icon='mdi:filter-check',
device_class='enum', options=('normal', 'wash', 'replace'),
value_fn=lambda value: (
value.lower() if isinstance(value, str) else value
)),
),
)
@@ -324,7 +469,7 @@ WATER_FILTER = Capability(
#
# No translation_key: aiLevel's values are plain digit strings, and
# select.py's _display() already renders an untranslated numeric string
# as-is -- there's nothing a strings.json entry adds that's worth maintaining
# as-is -- there's nothing a catalog entry adds that's worth maintaining
# against an unknown, growing number of future levels.
@@ -371,14 +516,14 @@ AI_ENERGY_LEVEL = Capability(
# cold-tier href, the same reload already required to fix which
# platform got picked in that case.
SwitchDesc(key='ai_energy_level', field='aiLevel',
name='AI energy level', icon='mdi:leaf',
icon='mdi:leaf',
entity_category='config',
value_fn=lambda v: v != '0',
exists_fn=lambda rep, resources: (
len(_ai_energy_supported_levels(rep)) == 1),
write_fn=_ai_energy_level_switch_write),
SelectDesc(key='ai_energy_level', field='aiLevel',
name='AI energy level', icon='mdi:leaf',
icon='mdi:leaf',
entity_category='config',
options=_ai_energy_level_options,
exists_fn=lambda rep, resources: (
@@ -394,7 +539,6 @@ FIRMWARE_UPDATE = Capability(
BinarySensorDesc(
key='firmware_update',
field='x.com.samsung.da.newVersionAvailable',
name='Firmware update available',
device_class='update',
entity_category='diagnostic',
value_fn=lambda v: str(v).lower() == 'true' if v is not None else None,
@@ -407,21 +551,21 @@ SELF_CHECK = Capability(
poll_tier='cold',
entities=(
SensorDesc(key='selfcheck_status', field='x.com.samsung.da.status',
name='Self-check status', icon='mdi:stethoscope',
icon='mdi:stethoscope',
entity_category='diagnostic'),
SensorDesc(key='selfcheck_result', field='x.com.samsung.da.result',
name='Self-check result', icon='mdi:clipboard-check-outline',
icon='mdi:clipboard-check-outline',
entity_category='diagnostic'),
# List of error codes from the last self-check; joined for display.
# Not every fridge reports the field, hence the exists_fn.
SensorDesc(key='selfcheck_error', field='x.com.samsung.da.error',
name='Self-check error', icon='mdi:alert-circle-outline',
icon='mdi:alert-circle-outline',
entity_category='diagnostic',
exists_fn=lambda rep, resources: (
not rep or 'x.com.samsung.da.error' in rep),
value_fn=lambda v: (', '.join(v) if v else None) if isinstance(v, list) else v),
ButtonDesc(key='selfcheck_start', field='', name='Start self-check',
payload='Start', icon='mdi:play-circle-outline',
ButtonDesc(key='selfcheck_start', field='', payload='Start',
icon='mdi:play-circle-outline',
entity_category='diagnostic',
write_fn=lambda p, rep, href=None: (
['selfcheck', 'vs', '0'], {'x.com.samsung.da.status': p})),
@@ -61,7 +61,6 @@ COOKTOP_POWER = Capability(
BinarySensorDesc(
key='power_state',
field='x.com.samsung.da.power',
name='Power state',
device_class='power',
icon='mdi:stove',
value_fn=lambda value: str(value).lower() == 'on',
@@ -83,7 +82,6 @@ COOKTOP_MODE = Capability(
BinarySensorDesc(
key='any_burner_active',
field='x.com.samsung.da.options',
name='Any burner active',
device_class='running',
icon='mdi:fire',
value_fn=_any_burner_active,
@@ -92,7 +90,8 @@ COOKTOP_MODE = Capability(
SensorDesc(
key=f'burner_{slot}_state',
field='x.com.samsung.da.options',
name=f'Burner {slot} state',
translation_key='burner_state',
translation_placeholders={'number': str(slot)},
icon='mdi:gas-burner',
value_fn=lambda options, slot=slot: _option_value(
options, f'OperationState{slot}'
@@ -109,14 +108,12 @@ COOKTOP_MODE = Capability(
SensorDesc(
key='main_timer_state',
field='x.com.samsung.da.options',
name='Timer state',
icon='mdi:timer-outline',
value_fn=lambda options: _option_value(options, 'MainTimerState'),
),
SensorDesc(
key='main_timer_current',
field='x.com.samsung.da.options',
name='Timer current value',
icon='mdi:timer-sand',
enabled_default=False,
value_fn=lambda options: _int_or_none(
@@ -134,7 +131,6 @@ COOKTOP_CONNECTED = Capability(
BinarySensorDesc(
key='cloud_connected',
field='x.com.samsung.da.connected',
name='Cloud connected',
device_class='connectivity',
entity_category='diagnostic',
value_fn=lambda value: str(value).lower() == 'on',
@@ -150,35 +146,30 @@ PAIRED_HOOD_STATUS = Capability(
BinarySensorDesc(
key='paired_hood_connected',
field='connectionState',
name='Paired hood connected',
device_class='connectivity',
value_fn=lambda value: str(value).lower() == 'connected',
),
BinarySensorDesc(
key='paired_hood_power',
field='power',
name='Paired hood power',
device_class='running',
value_fn=lambda value: str(value).lower() == 'on',
),
SensorDesc(
key='paired_hood_fan_speed',
field='fanSpeed',
name='Paired hood fan speed',
icon='mdi:fan',
value_fn=_int_or_none,
),
BinarySensorDesc(
key='paired_hood_light',
field='lampState',
name='Paired hood light',
device_class='light',
value_fn=lambda value: str(value).lower() == 'on',
),
SensorDesc(
key='paired_hood_model',
field='micomModelId',
name='Paired hood model',
icon='mdi:information-outline',
entity_category='diagnostic',
enabled_default=False,
@@ -186,7 +177,6 @@ PAIRED_HOOD_STATUS = Capability(
SensorDesc(
key='paired_hood_firmware',
field='firmwareVersion',
name='Paired hood firmware',
icon='mdi:chip',
entity_category='diagnostic',
enabled_default=False,
@@ -0,0 +1,85 @@
"""Capabilities for the Samsung dehumidifier family (TP1X_DA_AC_DHM-class,
issue #88, model AY18CG7500GED).
Same DA_AC_ board family as the room-AC models in airconditioner.py (shared
power/energy/filter/auto-clean/mute-once resource shapes), but target
humidity -- not temperature -- is this device's primary control, and there
is no climate composite: power, mode, and humidity are exposed as separate
entities rather than folded into one card.
"""
from ..capability import Capability
from ..entities import NumberDesc, SelectDesc, SensorDesc
from .common import int_or_none
def _first_mode(rep):
"""Representative scalar for the operating-mode select. `modes` is a
single-element list on every dump seen so far, mirroring
airconditioner._first_mode's handling of the same field shape."""
modes = rep.get('x.com.samsung.da.modes')
if isinstance(modes, (list, tuple)):
return modes[0] if modes else None
return modes
MODE = Capability(
href='/mode/vs/0',
poll_tier='warm',
entities=(
SelectDesc(key='operating_mode', rep_fn=_first_mode,
icon='mdi:tune-variant',
options_field='x.com.samsung.da.supportedModes',
write_fn=lambda p, rep, href=None: (
['mode', 'vs', '0'], {'x.com.samsung.da.modes': [p]})),
),
)
# Target humidity is this device's primary control (the issue-#88 dump's
# equivalent of a thermostat setpoint). No min/max range field is present in
# any dump seen so far -- native_min/native_max are deliberately left unset
# so the number entity falls back to HA's own 0-100 default, the natural
# bound for a percentage field, rather than a bound guessed from one unit's
# spec sheet (see the adding-device-support skill's "never hard-code the one
# dump's values" section). Step comes live from the device's own `increment`
# field.
HUMIDITY = Capability(
href='/humidity/vs/0',
poll_tier='warm',
entities=(
SensorDesc(key='humidity', field='x.com.samsung.da.humidity',
device_class='humidity', unit='%', state_class='measurement',
value_fn=int_or_none),
NumberDesc(key='target_humidity', field='x.com.samsung.da.desiredHumidity',
device_class='humidity', unit='%',
icon='mdi:water-percent',
entity_category='config',
value_fn=int_or_none,
step_fn=lambda rep: int_or_none(rep.get('increment')) or 1,
write_fn=lambda p, rep, href=None: (
['humidity', 'vs', '0'],
{'x.com.samsung.da.desiredHumidity': str(int(round(float(p))))})),
),
)
# ---------------------------------------------------------------------------
# Dehumidifier-scoped coverage: vendor plumbing with no user-actionable state
# or no documented write contract, following the same 'don't guess' rule as
# airconditioner._AC_IGNORED (this is the same DA_AC_ board family). Not in
# the global ignored.IGNORED since some of these hrefs collide with other
# families' schemas.
# ---------------------------------------------------------------------------
_DHM_IGNORED = [
'/availablecontrolsets/vs/0', # opaque hex-encoded control-set bitmap (id: DHM)
'/da/softreset/vs/0', # soft-reset trigger plumbing
'/keepnormalstate/vs/0', # internal keep-normal flag
'/personality/presence/vs/0', # presence-personalization plumbing (empty item value)
'/reserverulesets/vs/0', # opaque hex-encoded schedule reservation blob
'/sensors/vs/0', # empty {} on this dump
'/welcome/humidity/vs/0', # welcome-mode plumbing (requestId/operatingStatus, inert)
# Only supportedModes ([Off, Sleep]) is present -- no live "current
# value" field on this dump to confirm the read/write contract, so per
# the 'don't guess' rule this is left unmodeled rather than assumed.
'/mode/convenient/vs/0',
]
COVERAGE = [Capability(href=h) for h in _DHM_IGNORED]
@@ -18,13 +18,13 @@ DISHWASHER_SETTINGS = Capability(
href='/dishwasher/vs/0',
entities=(
SwitchDesc(key='sanitize', field='x.com.samsung.da.sanitize',
name='Sanitize', icon='mdi:bacteria',
icon='mdi:bacteria',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
['dishwasher', 'vs', '0'],
{'x.com.samsung.da.sanitize': 'On' if p == 'On' else 'Off'})),
SelectDesc(key='heated_dry', field='x.com.samsung.da.heatedDry',
name='Smart Dry', icon='mdi:heat-wave',
icon='mdi:heat-wave',
options_field='x.com.samsung.da.supportedHeatedDry',
write_fn=lambda p, rep, href=None: (
['dishwasher', 'vs', '0'],
@@ -45,9 +45,9 @@ CYCLE_OPTIONS = Capability(
href='/course/vs/0',
entities=(
cycle_select(translation_key='dishwasher_cycle', icon='mdi:dishwasher'),
bool_option_switch('storm_wash', 'Storm Wash+', 'mdi:weather-lightning-rainy',
bool_option_switch('storm_wash', 'mdi:weather-lightning-rainy',
'StormWashZone'),
bool_option_switch('auto_release_dry', 'Auto release dry', 'mdi:door-open',
bool_option_switch('auto_release_dry', 'mdi:door-open',
'AutoDoorRelease', gate_on_presence=True),
),
)
@@ -61,10 +61,10 @@ DIAGNOSIS = Capability(
poll_tier='cold',
entities=(
SensorDesc(key='diagnosis_status', field='x.com.samsung.da.diagnosisStart',
name='Diagnosis status', icon='mdi:stethoscope',
icon='mdi:stethoscope',
entity_category='diagnostic'),
ButtonDesc(key='diagnosis_start', field='', name='Start diagnosis',
payload='Start', icon='mdi:play-circle-outline',
ButtonDesc(key='diagnosis_start', field='', payload='Start',
icon='mdi:play-circle-outline',
entity_category='diagnostic',
write_fn=lambda p, rep, href=None: (
['diagnosis', 'vs', '0'], {'x.com.samsung.da.diagnosisStart': p})),
@@ -76,7 +76,7 @@ OPERATION_ORIGIN = Capability(
poll_tier='cold',
entities=(
SensorDesc(key='operation_origin', field='origin',
name='Last operation source', icon='mdi:remote',
icon='mdi:remote',
entity_category='diagnostic'),
),
)
@@ -24,13 +24,13 @@ DRYER_SETTINGS = Capability(
poll_tier='warm',
entities=(
SensorDesc(key='dry_level', field='x.com.samsung.da.dryLevel',
name='Dry level', icon='mdi:water-percent'),
icon='mdi:water-percent'),
SensorDesc(key='dry_time', field='x.com.samsung.da.dryTime',
name='Dry time', icon='mdi:timer'),
icon='mdi:timer'),
SensorDesc(key='dryer_type', field='x.com.samsung.da.dryerType',
name='Dryer type', icon='mdi:tumble-dryer'),
icon='mdi:tumble-dryer'),
SwitchDesc(key='wrinkle_prevent', field='x.com.samsung.da.wrinklePrevent',
name='Wrinkle prevent', icon='mdi:iron',
icon='mdi:iron',
value_fn=lambda v: v == 'On',
write_fn=_wrinkle_write),
),
@@ -42,7 +42,10 @@ DRYER_SETTINGS = Capability(
# under entity.select.dryer_cycle (Table_03, DV5000-class, captured
# 2026-05-29). Codes 0x21 and 0x4C appear in the issue #14 DV90BB5245AES1
# editCourseList but aren't identified yet -- they render as the raw code
# until named. The /st/dryercourse/vs/0 resource re-encodes the same selected
# until named. Codes '01' Normal and '06' Time dry were confirmed on a
# DVE50A8600V/A3 (also Table_03) by selecting each cycle on the physical
# appliance and reading back the raw code from the entity's state (issue
# #80). The /st/dryercourse/vs/0 resource re-encodes the same selected
# course and is ignored (ignored.py) -- the mirror of how /st/washercourse/vs/0
# is ignored for washers.
DRYER_COURSE = Capability(
@@ -58,6 +61,6 @@ DRYER_DIAGNOSIS = Capability(
poll_tier='warm',
entities=(
SensorDesc(key='diagnosis', field='x.com.samsung.da.diagnosisStart',
name='Diagnosis', entity_category='diagnostic'),
entity_category='diagnostic'),
),
)
@@ -25,7 +25,7 @@ from ..entities import (
from .common import normalize_temp_unit
# Display names for the beverage zone, flex zone, ice type, and
# ice-making-status enums below live in strings.json / translations/en.json,
# ice-making-status enums below live in translations/en.json,
# keyed by the lowercased raw device value — select.py and SensorDesc.options
# normalize to lowercase for HA's translation lookup and map back to this
# original casing before writing to the device.
@@ -55,26 +55,56 @@ TEMP_CURRENT_GENERIC = Capability(
poll_tier='warm',
entities=(
SensorDesc(key='temperature', field='temperature',
name=None, icon='mdi:thermometer',
translation_key='instance_temperature',
use_instance_name=True, icon='mdi:thermometer',
device_class='temperature', unit_fn=_temp_unit,
state_class='measurement'),
),
)
TEMP_SETPOINT_GENERIC = Capability(
def _temp_setpoint_write(p, rep, href=None, resources=None):
"""Write temperature — prefer vendor /temperatures/vs/0 when available,
fall back to direct OCF /temperature/desired/ write otherwise.
Samsung fridges expose both OCF-standard /temperature/desired/* and vendor
/temperatures/vs/0. On some models only the vendor path commits the change;
on others both work. Using the vendor path when present is always correct.
Item IDs follow the Samsung convention: "0" = Freezer, "1" = Fridge/Cooler.
"""
if not href:
return None
if resources and '/temperatures/vs/0' in resources:
if '/cooler/' in href:
item_id = '1'
elif '/freezer/' in href:
item_id = '0'
else:
return None
return (
['temperatures', 'vs', '0'],
{'x.com.samsung.da.items': [
{'x.com.samsung.da.id': item_id,
'x.com.samsung.da.desired': str(int(round(float(p))))}
]}
)
return (
[s for s in href.strip('/').split('/') if s],
{'temperature': int(round(float(p)))}
)
TEMP_SETPOINT = Capability(
href=None,
href_prefix='/temperature/desired/',
strip_prefix_in_key=True,
poll_tier='warm',
entities=(
NumberDesc(key='setpoint', field='temperature',
name=None, device_class='temperature', unit_fn=_temp_unit,
translation_key='instance_setpoint',
use_instance_name=True, device_class='temperature', unit_fn=_temp_unit,
native_min=-20.0, native_max=50.0,
range_field='range', entity_category='config',
write_fn=lambda p, rep, href=None: (
[s for s in href.strip('/').split('/') if s],
{'temperature': int(round(float(p)))}
) if href else None),
write_fn=_temp_setpoint_write),
),
)
@@ -87,7 +117,7 @@ ICEMAKER_NIGHTTIME = Capability(
poll_tier='warm',
entities=(
SwitchDesc(key='ice_night_mode', field='ice.night.status',
name='Nighttime ice quiet mode', icon='mdi:weather-night',
icon='mdi:weather-night',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
@@ -101,8 +131,9 @@ ICEMAKER_NIGHTTIME = Capability(
# /icemaker/status/vs/0 is kept as exact-href cap and binds first.
# /icemaker/nighttime/vs/0 is excluded by match_fn (lacks iceMaker.state).
#
# Entity names are derived from x.com.samsung.da.iceMaker.name ("CUBED_ICE",
# "ICE_BITES") via name_field, not the href's "one"/"two" segment -- these two
# Entity names interpolate x.com.samsung.da.iceMaker.name ("CUBED_ICE",
# "ICE_BITES") -- read via name_field, reaching the translated name as the
# {instance_name} placeholder -- not the href's "one"/"two" segment. These two
# ice makers are independent on/off toggles that can both be enabled at once
# (issue #27), so they stay separate entities rather than a single ice-type
# select, but users still want them labeled with the device's own names.
@@ -123,17 +154,18 @@ ICEMAKER_GENERIC = Capability(
entities=(
SensorDesc(key='making_status',
field='x.com.samsung.da.iceMaker.iceMakingStatus',
name=None, icon='mdi:cube-outline',
use_instance_name=True, icon='mdi:cube-outline',
device_class='enum',
options=('icestatus_stop', 'icestatus_run'),
translation_key='ice_making_status',
value_fn=lambda v: v.lower() if isinstance(v, str) else v),
SwitchDesc(key='enabled', field='x.com.samsung.da.iceMaker.state',
name=None, icon='mdi:cube-outline',
translation_key='instance_enabled',
use_instance_name=True, icon='mdi:cube-outline',
value_fn=lambda v: v == 'On',
write_fn=_icemaker_write('x.com.samsung.da.iceMaker.state')),
SelectDesc(key='type', field='x.com.samsung.da.iceType.desired',
name=None, icon='mdi:cube-outline',
use_instance_name=True, icon='mdi:cube-outline',
translation_key='ice_type',
entity_category='config',
options_field='x.com.samsung.da.iceType.supported',
@@ -151,7 +183,7 @@ DOOR_ALERT = Capability(
poll_tier='warm',
entities=(
SelectDesc(key='door_alert', field='alert.door',
name='Door alarm', icon='mdi:bell-alert',
icon='mdi:bell-alert',
translation_key='door_alert',
entity_category='config',
options_field='supportedAlert.door',
@@ -177,12 +209,12 @@ STATUS_LOCK = Capability(
poll_tier='warm',
entities=(
SwitchDesc(key='auto_door_opener', field='x.com.samsung.da.ado.devicecontrol',
name='Auto door opener', icon='mdi:door-open',
icon='mdi:door-open',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=_status_lock_write('x.com.samsung.da.ado.devicecontrol')),
SwitchDesc(key='fridge_sound', field='x.com.samsung.da.device.sound',
name='Sound', icon='mdi:volume-high',
icon='mdi:volume-high',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=_status_lock_write('x.com.samsung.da.device.sound')),
@@ -207,7 +239,7 @@ DEFROST_DELAY = Capability(
poll_tier='warm',
entities=(
SwitchDesc(key='defrost_delay', field='x.com.samsung.da.delayDefrost',
name='Defrost delay', icon='mdi:snowflake-off',
icon='mdi:snowflake-off',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
@@ -229,7 +261,7 @@ DEFROST_BLOCK_STATUS = Capability(
poll_tier='warm',
entities=(
BinarySensorDesc(key='defrost_active', field='x.com.samsung.da.modes',
name='Defrost active', icon='mdi:snowflake-melt',
icon='mdi:snowflake-melt',
entity_category='diagnostic',
value_fn=lambda modes: bool(modes) and modes[0] == 'DEFROST_BLOCK_ON'),
),
@@ -252,12 +284,12 @@ REFRIGERATION = Capability(
poll_tier='warm',
entities=(
SwitchDesc(key='rapid_fridge', field='x.com.samsung.da.rapidFridge',
name='Rapid fridge', icon='mdi:fridge-industrial',
icon='mdi:fridge-industrial',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=_refrigeration_write('x.com.samsung.da.rapidFridge')),
SwitchDesc(key='rapid_freezing', field='x.com.samsung.da.rapidFreezing',
name='Rapid freezing', icon='mdi:snowflake',
icon='mdi:snowflake',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=_refrigeration_write('x.com.samsung.da.rapidFreezing')),
@@ -279,7 +311,7 @@ AUTOFILL = Capability(
poll_tier='warm',
entities=(
SwitchDesc(key='autofill', field='x.com.samsung.da.autofill',
name='Autofill pitcher', icon='mdi:cup-water',
icon='mdi:cup-water',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=_autofill_write),
@@ -295,7 +327,7 @@ WELCOME_LIGHTING = Capability(
poll_tier='warm',
entities=(
SwitchDesc(key='welcome_lighting', field='status',
name='Welcome lighting', icon='mdi:motion-sensor',
icon='mdi:motion-sensor',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
@@ -386,15 +418,15 @@ CABINET_LIGHT_ENHANCED = Capability(
poll_tier='warm',
entities=(
SelectDesc(key='day_brightness', field='level.brightness.daytime',
name='Cabinet brightness', icon='mdi:brightness-5',
translation_key='brightness_level',
icon='mdi:brightness-5',
translation_key='day_brightness',
entity_category='config',
options=_NIGHT_BRIGHTNESS_OPTIONS,
write_fn=lambda p, rep, href=None: (
['cabinet', 'light', 'enhanced', 'vs', '0'],
{'level.brightness.daytime': p})),
SelectDesc(key='brightness_level', field='level.brightness.nighttime',
name='Night brightness', icon='mdi:brightness-4',
icon='mdi:brightness-4',
translation_key='brightness_level',
entity_category='config',
options=_NIGHT_BRIGHTNESS_OPTIONS,
@@ -402,12 +434,12 @@ CABINET_LIGHT_ENHANCED = Capability(
['cabinet', 'light', 'enhanced', 'vs', '0'],
{'level.brightness.nighttime': p})),
TimeDesc(key='night_start', field='',
name='Night light start', icon='mdi:clock-start',
icon='mdi:clock-start',
entity_category='config',
rep_fn=_night_start_value,
write_fn=_night_start_write),
TimeDesc(key='night_end', field='',
name='Night light end', icon='mdi:clock-end',
icon='mdi:clock-end',
entity_category='config',
rep_fn=_night_end_value,
write_fn=_night_end_write),
@@ -431,11 +463,11 @@ CABINET_LIGHT = Capability(
poll_tier='warm',
entities=(
SwitchDesc(key='cabinet_light_switch', field='x.com.samsung.da.lightControl',
name='Cabinet light', icon='mdi:fridge-outline',
icon='mdi:fridge-outline',
value_fn=lambda v: v == 'On',
write_fn=_cabinet_light_write),
SwitchDesc(key='cabinet_light_dim', field='light.dimming.status',
name='Brighten gradually', icon='mdi:brightness-auto',
icon='mdi:brightness-auto',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
@@ -459,7 +491,7 @@ SABBATH = Capability(
poll_tier='warm',
entities=(
SwitchDesc(key='sabbath_mode', field='x.com.samsung.da.sabbathMode',
name='Sabbath mode', icon='mdi:hands-pray',
icon='mdi:hands-pray',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=_sabbath_write),
@@ -479,7 +511,7 @@ BEVERAGE_ZONE = Capability(
poll_tier='warm',
entities=(
SelectDesc(key='beverage_zone_mode', field='roomDesiredMode',
name='Beverage zone mode', icon='mdi:glass-wine',
icon='mdi:glass-wine',
translation_key='beverage_zone_mode',
entity_category='config',
options_field='roomSupportedModes',
@@ -507,7 +539,7 @@ PANTRY_ZONE = Capability(
poll_tier='warm',
entities=(
SelectDesc(key='pantry_zone_mode', field='x.com.samsung.da.mode',
name='Pantry zone mode', icon='mdi:glass-wine',
icon='mdi:glass-wine',
translation_key='pantry_zone_mode',
entity_category='config',
options_field='x.com.samsung.da.supportedOptions',
@@ -554,7 +586,7 @@ FLEX_ZONE = Capability(
poll_tier='warm',
entities=(
SelectDesc(key='flex_zone_mode',
name='Flex zone mode', icon='mdi:thermostat',
icon='mdi:thermostat',
translation_key='flex_zone_mode',
entity_category='config',
options_field='x.com.samsung.da.supportedOptions',
@@ -569,14 +601,27 @@ FLEX_ZONE = Capability(
# Generic door pattern capability (href=None — use as pattern_cap only)
# ---------------------------------------------------------------------------
def _door_open_state(rep):
"""Most /door/* resources report bare `openState`, but the
ARTIK051_DONGLE_REF family's /door/onedoorfreezer/vs/0 (issues #77, #83)
reports the vendor-prefixed `x.com.samsung.da.openState` instead. This
capability still binds either way (href_prefix match doesn't care about
field names), but a plain `field=` lookup against the wrong key means
the entity exists and is permanently unavailable -- check both."""
v = rep.get('openState')
if v is None:
v = rep.get('x.com.samsung.da.openState')
return v == 'Open'
DOOR_GENERIC = Capability(
href=None,
href_prefix='/door/',
poll_tier='hot',
entities=(
BinarySensorDesc(key='open', field='openState',
name=None, device_class='door',
value_fn=lambda v: v == 'Open'),
BinarySensorDesc(key='open', rep_fn=_door_open_state,
translation_key='instance_open',
use_instance_name=True, device_class='door'),
),
)
@@ -604,7 +649,7 @@ DOORS_FALLBACK = Capability(
poll_tier='hot',
entities=(
BinarySensorDesc(key='door_open', field='x.com.samsung.da.items',
name='Door', device_class='door',
device_class='door',
value_fn=lambda items: any(
i.get('x.com.samsung.da.openState') == 'Open'
for i in (items or []))),
@@ -636,13 +681,13 @@ TEMPERATURES_FALLBACK = Capability(
poll_tier='warm',
entities=(
SensorDesc(key='freezer_temperature', field='x.com.samsung.da.items',
name='Freezer temperature', icon='mdi:thermometer',
icon='mdi:thermometer',
device_class='temperature', state_class='measurement',
unit_fn=lambda rep: _temp_item_unit(
rep.get('x.com.samsung.da.items'), 'Freezer'),
value_fn=lambda items: _temp_item_value(items, 'Freezer')),
SensorDesc(key='fridge_temperature', field='x.com.samsung.da.items',
name='Fridge temperature', icon='mdi:thermometer',
icon='mdi:thermometer',
device_class='temperature', state_class='measurement',
unit_fn=lambda rep: _temp_item_unit(
rep.get('x.com.samsung.da.items'), 'Fridge'),
@@ -665,7 +710,7 @@ ICEMAKER_STATUS_FALLBACK = Capability(
poll_tier='warm',
entities=(
SwitchDesc(key='ice_maker_enabled', field='x.com.samsung.da.iceMaker',
name='Ice maker', icon='mdi:cube-outline',
icon='mdi:cube-outline',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
['icemaker', 'status', 'vs', '0'],
@@ -695,17 +740,17 @@ REFRIGERATION_FALLBACK = Capability(
poll_tier='warm',
entities=(
BinarySensorDesc(key='defrost_active', field='defrost',
name='Defrost active', icon='mdi:snowflake-melt',
icon='mdi:snowflake-melt',
entity_category='diagnostic',
value_fn=lambda v: bool(v),
exists_fn=lambda rep, resources: '/defrost/block/vs/0' not in resources),
BinarySensorDesc(key='rapid_fridge', field='rapidCool',
name='Rapid fridge', icon='mdi:fridge-industrial',
icon='mdi:fridge-industrial',
entity_category='config',
value_fn=lambda v: bool(v),
exists_fn=lambda rep, resources: '/refrigeration/vs/0' not in resources),
BinarySensorDesc(key='rapid_freezing', field='rapidFreeze',
name='Rapid freezing', icon='mdi:snowflake',
icon='mdi:snowflake',
entity_category='config',
value_fn=lambda v: bool(v),
exists_fn=lambda rep, resources: '/refrigeration/vs/0' not in resources),
@@ -131,4 +131,12 @@ IGNORED: list[Capability] = [
# parallel write-capable capability around an unverified generic OCF
# write contract.
Capability(href='/operational/state/0'),
# Cooktop guided-cooking/recipe status (issue #86, TP1X_DA-KS-COOKTOP
# family): sequenceNumber, operationBurnerNumber, a stageInfo block, and
# a textData.menu string -- every field empty/zero on the only dump
# seen so far (device idle, no guided-cooking program active). Same
# "don't guess" treatment as the microwave family's /recipe/cook/vs/0.
# Revisit if a dump with an active recipe surfaces.
Capability(href='/cooktop/recipe/status/vs/0'),
]
@@ -21,6 +21,7 @@ Door-LED keys use NO `x.com.samsung.da.` prefix -- `setBrightness` /
"""
from datetime import time as dt_time
from ...catalog import has_entity_translation
from ..capability import Capability
from ..entities import NumberDesc, SelectDesc, SensorDesc, SwitchDesc, TimeDesc
@@ -60,30 +61,30 @@ DOOR_LED = Capability(
href='/doorled/light/vs/0',
entities=(
SelectDesc(key='led_brightness', field='setBrightness',
name='Door LED brightness', icon='mdi:brightness-6',
icon='mdi:brightness-6',
entity_category='config',
options=_LED_LEVELS, write_fn=_led_brightness_write),
SwitchDesc(key='led_night_light', field='setNightLight',
name='Door LED night light', icon='mdi:weather-night',
icon='mdi:weather-night',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=_led_night_write),
SelectDesc(key='led_night_brightness', field='setNightLightBrightness',
name='Door LED night brightness', icon='mdi:brightness-4',
icon='mdi:brightness-4',
entity_category='config',
options=_LED_LEVELS,
write_fn=lambda p, rep, href=None: (
['doorled', 'light', 'vs', '0'],
{'setNightLightBrightness': p})),
TimeDesc(key='led_night_start', field='setNightLightTimeStart',
name='Door LED night start', icon='mdi:clock-start',
icon='mdi:clock-start',
entity_category='config',
value_fn=_parse_hm,
write_fn=lambda p, rep, href=None: (
['doorled', 'light', 'vs', '0'],
{'setNightLightTimeStart': f'{p.hour:02d}:{p.minute:02d}'})),
TimeDesc(key='led_night_end', field='setNightLightTimeEnd',
name='Door LED night end', icon='mdi:clock-end',
icon='mdi:clock-end',
entity_category='config',
value_fn=_parse_hm,
write_fn=lambda p, rep, href=None: (
@@ -96,7 +97,7 @@ SOUND_MODE = Capability(
href='/settings/sound/mode/vs/0',
entities=(
SelectDesc(key='sound_mode', field='mode',
name='Sound mode', icon='mdi:volume-high',
icon='mdi:volume-high',
entity_category='config',
options=_SOUND_MODES, write_fn=_sound_mode_write),
),
@@ -106,7 +107,7 @@ SOUND_VOLUME = Capability(
href='/settings/sound/volume/vs/0',
entities=(
NumberDesc(key='sound_volume', field='level',
name='Sound volume', icon='mdi:volume-medium',
icon='mdi:volume-medium',
entity_category='config',
native_min=0, native_max=15, step=5,
value_fn=lambda v: int(v) if v is not None else None,
@@ -127,13 +128,13 @@ BUZZER_SOUND = Capability(
href='/buzzersound/vs/0',
entities=(
SelectDesc(key='buzzer_sound', field='setBuzzerSound',
name='Buzzer sound', icon='mdi:volume-high',
icon='mdi:volume-high',
entity_category='config',
options_field='supportedBuzzerSound',
write_fn=lambda p, rep, href=None: (
['buzzersound', 'vs', '0'], {'setBuzzerSound': p})),
SelectDesc(key='finish_sound', field='setFinishSound',
name='Finish sound', icon='mdi:bell-ring',
icon='mdi:bell-ring',
entity_category='config',
exists_fn=lambda rep, resources: 'supportedFinishSound' in rep,
options_field='supportedFinishSound',
@@ -291,23 +292,22 @@ def cycle_select(*, translation_key, icon, table_href=None):
suffixes translation_key with the device's own course-table id, read
from /st/washercourse/vs/0 or /st/dryercourse/vs/0's
x.com.samsung.da.st.courseTable (e.g. 'washer_cycle' + 'Table_02' ->
'washer_cycle_table_02'). No table id available at all -- the href
absent or empty -- gets no translation_key, i.e. the raw course code
displayed as-is.
'washer_cycle_table_02'). An absent or unrecognized table id gets the
name-only ``cycle`` translation key while the raw course code remains
visible and writable.
This matters because course codes are NOT guaranteed consistent across
board generations sharing the same /course/vs/0 contract: every code in
washer_cycle_table_02 was confirmed against Table_02-reporting devices
(DA_WM_TP1/TP2 boards); FlexWash's older DA_WM_A51 board reports
Table_00 instead, so the same hex code could mean a different course
there for all we've verified. Building the key from whatever table the
device actually reports, rather than gating a single hardcoded key on
an exact match, means a table we haven't built translations for yet
(like Table_00) just falls through Home Assistant's own missing-
translation handling to the same raw-code display -- exactly what
happens today for any individual code within a table's translations
that isn't populated yet -- and adding one later needs new strings.json
entries, not a code change here.
there for all we've verified. So a table-specific key is used only when
the shipped catalog actually has one; any other table (Table_00 today,
whatever ships next) falls back to the name-only ``cycle`` key, which
shows the raw course code rather than a label borrowed from another
board generation. Translating a new table is therefore a
translations-only change -- add the ``<family>_cycle_<table>`` entry and
this resolver picks it up.
Left at its default for dishwasher, which has no equivalent table-id
resource in any dump seen and no evidence its course codes vary by
@@ -318,10 +318,13 @@ def cycle_select(*, translation_key, icon, table_href=None):
if table_href is not None:
def key(resources):
table = _table_id(resources, table_href)
return f'{translation_key}_{table.lower()}' if table else None
if not isinstance(table, str) or not table:
return 'cycle'
candidate = f'{translation_key}_{table.lower()}'
return candidate if has_entity_translation('select', candidate) else 'cycle'
return SelectDesc(
key='cycle', name='Cycle', icon=icon, translation_key=key,
key='cycle', icon=icon, translation_key=key,
options=cycle_options,
exists_fn=lambda rep, resources: bool(cycle_options(resources)),
rep_fn=lambda rep: option_value(rep.get('x.com.samsung.da.options'), 'Course'),
@@ -360,8 +363,8 @@ def bool_option_exists(prefix):
rep.get('x.com.samsung.da.options'), prefix) is not None
def bool_option_switch(key, name, icon, prefix, *, entity_category=None,
gate_on_presence=False, validate_fn=None):
def bool_option_switch(key, icon, prefix, *, entity_category=None,
gate_on_presence=False, validate_fn=None):
"""A SwitchDesc over a '<prefix>_On'/'<prefix>_Off' options[] token.
gate_on_presence self-gates the entity off on models that never report
@@ -373,7 +376,7 @@ def bool_option_switch(key, name, icon, prefix, *, entity_category=None,
building one, if needed, is the caller's job.
"""
return SwitchDesc(
key=key, name=name, icon=icon, entity_category=entity_category,
key=key, icon=icon, entity_category=entity_category,
exists_fn=bool_option_exists(prefix) if gate_on_presence else None,
rep_fn=bool_option_value(prefix),
write_fn=bool_option_write(prefix),
@@ -396,7 +399,6 @@ JOB_BEGINNING_STATUS = Capability(
entities=(
SensorDesc(key='job_beginning_status',
field='x.com.samsung.da.currentStatus',
name='Job beginning status',
entity_category='diagnostic'),
),
)
@@ -2,7 +2,8 @@
Shared by dryer/dishwasher/oven/washer families.
"""
from datetime import datetime, timezone, timedelta
import math
from datetime import datetime, timedelta, timezone
from ..capability import Capability
from ..entities import BinarySensorDesc, ButtonDesc, NumberDesc, SensorDesc
@@ -28,17 +29,41 @@ def _int(v):
return None
def _is_active(rep):
"""Check if appliance is actively running and cycle is not finished."""
return (
_SAMSUNG_STATE_TO_OCF.get(rep.get('x.com.samsung.da.state')) == 'active'
and rep.get('x.com.samsung.da.progress') != 'Finish'
)
def _remaining_seconds(raw):
"""'HH:MM:SS' (or 'MM:SS') -> total seconds, or None."""
if not isinstance(raw, str):
return None
try:
parts = [int(p) for p in raw.split(':')]
except (ValueError, TypeError):
return None
if len(parts) == 3:
h, m, s = parts
elif len(parts) == 2:
h, m, s = 0, *parts
else:
return None
return h * 3600 + m * 60 + s
def _delay_hours(v):
"""delayStartTime is a duration until the cycle starts, not a
wall-clock time -- "01:00" means "1 hour from when you press start",
not "1 AM"."""
if not v:
return 0.0
try:
h, m, s = v.split(':')
return int(h) + int(m) / 60 + int(s) / 3600
except Exception:
return None
total_seconds = _remaining_seconds(v)
if total_seconds is None:
return 0.0 if not v else None
return total_seconds / 3600.0
def _format_delay(hours):
@@ -53,27 +78,39 @@ def _delay_field(rep):
wall-clock time -- see _delay_hours). Write back whichever key the
device itself is using; default to delayStartTime for hardware that
reports neither yet (matches prior behavior)."""
return ('x.com.samsung.da.delayEndTime' if 'x.com.samsung.da.delayEndTime' in rep
else 'x.com.samsung.da.delayStartTime')
return (
'x.com.samsung.da.delayEndTime' if 'x.com.samsung.da.delayEndTime' in rep
else 'x.com.samsung.da.delayStartTime'
)
def _finish_time(remaining_str):
if not remaining_str:
def _finish_time(rep):
if not _is_active(rep):
return None
try:
h, m, s = remaining_str.split(':')
total_s = int(h) * 3600 + int(m) * 60 + int(s)
if total_s == 0:
return None
return datetime.now(timezone.utc) + timedelta(seconds=total_s)
except Exception:
total_s = _remaining_seconds(rep.get('x.com.samsung.da.remainingTime'))
if not total_s:
return None
return datetime.now(timezone.utc) + timedelta(seconds=total_s)
def _completion_minutes(rep):
"""Parse remaining time into minutes directly from device payload."""
raw = rep.get('x.com.samsung.da.remainingTime') or rep.get('remainingTime')
total_s = _remaining_seconds(raw)
if total_s is None:
return None
# Avoid the firmware bug where it freezes at 1 minute post-cycle
if rep.get('x.com.samsung.da.progress') == 'Finish':
return 0
return math.ceil(total_s / 60)
# Shared by dryer/dishwasher/oven/washer -- oven.py imports this directly
# rather than keeping its own copy, since both wrote the identical
# state='Ready' RMW.
STOP_BUTTON = ButtonDesc(key='stop', field='', name='Stop', payload='Ready',
STOP_BUTTON = ButtonDesc(key='stop', field='', payload='Ready',
icon='mdi:stop',
write_fn=lambda p, rep, href=None: (
['operational', 'state', 'vs', '0'],
@@ -84,46 +121,50 @@ OPERATIONAL_STATE = Capability(
poll_tier='hot',
entities=(
SensorDesc(key='machine_state', field='x.com.samsung.da.state',
name='Machine state', device_class='enum',
device_class='enum',
options=('idle', 'active', 'pause'),
translation_key='machine_state', value_fn=_to_ocf),
# cycle_active is a bool derived from machine_state; used by the
# adapter to gate oven writes (cycle_active_field='cycle_active').
# Harmless for non-oven appliances — just an extra bool in state.
# Samsung firmware keeps state='Run' after progress reaches 'Finish',
# so we also gate on progress to avoid a stuck 'Running' indication.
# Named 'Running' rather than 'Cycle active' -- this href (and the
# Named 'Running' in the catalog rather than 'Cycle active' -- this href (and the
# start/pause/stop buttons below) is shared across the dryer/
# dishwasher/oven/washer families, and 'cycle' is laundry-specific
# vocabulary that doesn't fit an oven's bake/roast/etc.
BinarySensorDesc(key='cycle_active', device_class='running',
name='Running',
rep_fn=lambda rep: (
_SAMSUNG_STATE_TO_OCF.get(rep.get('x.com.samsung.da.state')) == 'active'
and rep.get('x.com.samsung.da.progress') != 'Finish'
)),
SensorDesc(key='progress', name='Progress', icon='mdi:progress-wrench',
SensorDesc(key='progress', icon='mdi:progress-wrench',
rep_fn=lambda rep: (
'Idle' if _SAMSUNG_STATE_TO_OCF.get(rep.get('x.com.samsung.da.state')) != 'active'
else _progress(rep.get('x.com.samsung.da.progress'))
)),
SensorDesc(key='progress_percentage',
name='Progress percent', unit='%', state_class='measurement',
unit='%', state_class='measurement',
rep_fn=lambda rep: (
0 if _SAMSUNG_STATE_TO_OCF.get(rep.get('x.com.samsung.da.state')) != 'active'
else _int(rep.get('x.com.samsung.da.progressPercentage'))
)),
# Only show finish time when machine is actively running. Samsung
# firmware leaves a stale remainingTime after a cycle ends, and
# freezes it at '00:01:00' when progress reaches 'Finish'.
SensorDesc(key='finish_time', device_class='timestamp',
name='Estimated finish',
rep_fn=lambda rep: (
None if _SAMSUNG_STATE_TO_OCF.get(rep.get('x.com.samsung.da.state')) != 'active'
or rep.get('x.com.samsung.da.progress') == 'Finish'
else _finish_time(rep.get('x.com.samsung.da.remainingTime'))
)),
NumberDesc(key='delay_start_hours', name='Delay start', icon='mdi:timer-plus-outline',
rep_fn=_finish_time),
SensorDesc(key='completion_minutes',
icon='mdi:clock-outline', unit='min',
device_class='duration', state_class='measurement',
exists_fn=lambda rep, resources: _completion_minutes(rep) is not None,
rep_fn=_completion_minutes),
NumberDesc(key='delay_start_hours', icon='mdi:timer-plus-outline',
device_class='duration', unit='h',
native_min=0, native_max=24, step=1,
rep_fn=lambda rep: _delay_hours(
@@ -132,16 +173,17 @@ OPERATIONAL_STATE = Capability(
write_fn=lambda p, rep, href=None: (
['operational', 'state', 'vs', '0'],
{_delay_field(rep): _format_delay(p)})),
ButtonDesc(key='start', field='', name='Start', payload='Run',
ButtonDesc(key='start', field='', payload='Run',
icon='mdi:play',
write_fn=lambda p, rep, href=None: (
['operational', 'state', 'vs', '0'],
{'x.com.samsung.da.state': p})),
ButtonDesc(key='pause', field='', name='Pause', payload='Pause',
ButtonDesc(key='pause', field='', payload='Pause',
icon='mdi:pause',
write_fn=lambda p, rep, href=None: (
['operational', 'state', 'vs', '0'],
{'x.com.samsung.da.state': p})),
STOP_BUTTON,
),
)
@@ -228,25 +228,25 @@ OVEN_OPERATIONAL_STATE = Capability(
poll_tier='hot',
entities=(
SensorDesc(key='machine_state', field='x.com.samsung.da.state',
name='Machine state', icon='mdi:stove',
icon='mdi:stove',
device_class='enum', options=('idle', 'active', 'pause'),
translation_key='machine_state', value_fn=_to_ocf),
BinarySensorDesc(key='cycle_active', field='x.com.samsung.da.state',
name='Running', device_class='running',
device_class='running',
value_fn=lambda v: _SAMSUNG_STATE_TO_OCF.get(v) == 'active'),
SensorDesc(key='progress_percentage',
field='x.com.samsung.da.progressPercentage',
name='Progress percent', unit='%', state_class='measurement',
unit='%', state_class='measurement',
value_fn=_int),
SensorDesc(key='operation_time_minutes',
field='x.com.samsung.da.operationTime',
name='Operation time (minutes)', unit='min',
unit='min',
state_class='measurement', value_fn=_op_minutes),
SensorDesc(key='finish_time', field='x.com.samsung.da.remainingTime',
name='Estimated finish', device_class='timestamp',
device_class='timestamp',
value_fn=_finish_time),
NumberDesc(key='cook_time', field='x.com.samsung.da.operationTime',
name='Cook time', unit='min', native_min=0, native_max=1439,
unit='min', native_min=0, native_max=1439,
step=1.0, icon='mdi:timer', value_fn=_op_minutes,
write_fn=_cook_time_write),
STOP_BUTTON,
@@ -258,7 +258,7 @@ OVEN_CAVITY = Capability(
poll_tier='hot',
entities=(
SensorDesc(key='oven_state', field='x.com.samsung.da.state',
name='Cavity state'),
),
),
)
@@ -291,7 +291,7 @@ OVEN_SETPOINT = Capability(
entities=(
# NumberDesc first — test_oven_setpoint_write_is_read_modify_write uses entities[0]
NumberDesc(key='oven_setpoint', field='x.com.samsung.da.items',
name='Setpoint', device_class='temperature', unit_fn=_oven_temp_unit,
device_class='temperature', unit_fn=_oven_temp_unit,
native_min=float(SETPOINT_MIN_C), native_max=float(SETPOINT_MAX_C),
step=float(SETPOINT_STEP_C), icon='mdi:thermometer-chevron-up',
native_min_fn=lambda rep: float(_setpoint_bounds(rep)[0]),
@@ -301,7 +301,7 @@ OVEN_SETPOINT = Capability(
(items[0].get('x.com.samsung.da.desired') if items else None)),
write_fn=_oven_setpoint_write),
SensorDesc(key='current_temp_c', field='x.com.samsung.da.items',
name='Temperature', device_class='temperature',
device_class='temperature',
state_class='measurement', unit_fn=_oven_temp_unit,
value_fn=lambda items: _int(
(items[0].get('x.com.samsung.da.current') if items else None))),
@@ -313,7 +313,7 @@ OVEN_DOOR = Capability(
poll_tier='hot',
entities=(
BinarySensorDesc(key='door_open', field='x.com.samsung.da.items',
name='Door', device_class='door',
device_class='door',
value_fn=lambda items: (
items[0].get('x.com.samsung.da.openState') == 'Open'
if items else None)),
@@ -325,7 +325,7 @@ OVEN_CONNECTED = Capability(
poll_tier='warm',
entities=(
BinarySensorDesc(key='cloud_connected', field='x.com.samsung.da.connected',
name='Cloud connected', device_class='connectivity',
device_class='connectivity',
entity_category='diagnostic',
value_fn=lambda v: v == 'On'),
),
@@ -338,31 +338,37 @@ OVEN_CONNECTED = Capability(
# fields worth exposing.
OVEN_SPEC = Capability(href='/oven/spec/vs/0')
# Quick-recipe display blob (combi microwave, issue #121) -- a JSON-encoded
# string (language/menu/servingSize/option) with every field blank on the
# only dump seen, and no documented write contract. No entity to bind per
# the 'don't guess' rule; a bare Capability still marks the href covered.
OVEN_RECIPE_COOK = Capability(href='/recipe/cook/vs/0')
OVEN_MODE = Capability(
href='/mode/vs/0',
poll_tier='warm',
entities=(
# SelectDesc first — test_oven_mode_options_nonempty uses entities[0]
SelectDesc(key='oven_mode', field='x.com.samsung.da.modes',
name='Cooking mode', icon='mdi:tune',
icon='mdi:tune',
options=_OVEN_MODES,
value_fn=lambda v: v[0] if v else None,
write_fn=_oven_mode_write),
SwitchDesc(key='lamp', field='x.com.samsung.da.options',
name='Lamp', icon='mdi:track-light',
icon='mdi:track-light',
value_fn=lambda opts: _option_value(opts, 'UpperLamp') == 'On',
write_fn=_lamp_write),
SwitchDesc(key='sound', field='x.com.samsung.da.options',
name='Sound', icon='mdi:volume-high',
icon='mdi:volume-high',
entity_category='config',
value_fn=lambda opts: _option_value(opts, 'Sound') == 'On',
write_fn=_sound_write),
SwitchDesc(key='fast_preheat', field='x.com.samsung.da.options',
name='Fast preheat', icon='mdi:fire',
icon='mdi:fire',
value_fn=lambda opts: _option_value(opts, 'fastpreheat') == 'On',
write_fn=_fastpreheat_write),
SwitchDesc(key='natural_steam', field='x.com.samsung.da.options',
name='Natural steam', icon='mdi:kettle-steam',
icon='mdi:kettle-steam',
value_fn=lambda opts: _option_value(opts, 'NaturalSteam') == 'On',
write_fn=_naturalsteam_write),
),
@@ -24,7 +24,8 @@ write pattern already proven safe elsewhere in this codebase (oven setpoint,
icemaker toggles).
"""
from ..capability import Capability
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc, SwitchDesc
from .common import normalize_temp_unit
# Observed as high as 4 (this issue's dump); user-reported hardware with 5
# burners exists. Kept a little above both since exists_fn gates unused
@@ -52,6 +53,10 @@ def _burner_hot_surface_fn(i):
return lambda burner_list: get_state(burner_list) not in (None, 'normal')
def _burner_pan_detection_fn(i):
return lambda burner_list: bool(_burner_field_fn(i, 'panDetection')(burner_list))
def _power_level_options(resources):
spec = resources.get('/cooktop/spec/vs/0') or {}
return list(spec.get('supportedPowerLevelList') or [])
@@ -81,28 +86,63 @@ def _burner_entities(i):
n = i + 1
return (
SelectDesc(key=f'burner_{i}_power_level', field='burnerList',
name=f'Burner {n} power level', icon='mdi:knob',
icon='mdi:knob',
translation_key='range_burner_power_level',
translation_placeholders={'number': str(n)},
options=_power_level_options,
exists_fn=exists,
value_fn=_burner_field_fn(i, 'powerLevel'),
write_fn=_burner_power_level_write(i)),
SensorDesc(key=f'burner_{i}_state', field='burnerList',
name=f'Burner {n} state', icon='mdi:stove',
icon='mdi:stove',
translation_key='burner_state',
translation_placeholders={'number': str(n)},
exists_fn=exists,
value_fn=_burner_field_fn(i, 'operationState')),
BinarySensorDesc(key=f'burner_{i}_hot_surface', field='burnerList',
name=f'Burner {n} hot surface', device_class='heat',
device_class='heat',
translation_key='burner_hot_surface',
translation_placeholders={'number': str(n)},
exists_fn=exists,
value_fn=_burner_hot_surface_fn(i)),
BinarySensorDesc(key=f'burner_{i}_pan_detected', field='burnerList',
icon='mdi:pot',
translation_key='burner_pan_detected',
translation_placeholders={'number': str(n)},
entity_category='diagnostic',
exists_fn=exists,
value_fn=_burner_pan_detection_fn(i)),
)
def _child_lock_write(p, rep, href=None):
if p not in ('On', 'Off'):
return None
return ['cooktop', 'status', 'vs', '0'], {'childLock': p.lower()}
COOKTOP_STATUS = Capability(
href='/cooktop/status/vs/0',
poll_tier='hot',
entities=(
SensorDesc(key='cooktop_state', field='operationState',
name='Cooktop state', icon='mdi:pot-steam'),
icon='mdi:pot-steam'),
# The cooktop section's own on/off (issue #86) -- distinct from
# common.POWER's /power/0 or /power/vs/0, which some boards in this
# family (the range combo) additionally carry for the whole
# appliance. Read-only: no live device to confirm remotely turning
# a cooktop on wouldn't leave a burner active unattended.
BinarySensorDesc(key='cooktop_power', field='power',
device_class='power', icon='mdi:pot-steam',
value_fn=lambda v: str(v).lower() == 'on'),
# Safe to write -- a lock toggle, not a heat control -- via a
# direct single-field PUT (no RMW needed; unlike burnerList this
# is a lone scalar, not an array of siblings to preserve).
SwitchDesc(key='cooktop_child_lock', field='childLock',
device_class='lock', entity_category='config',
icon='mdi:lock',
value_fn=lambda v: str(v).lower() == 'on',
write_fn=_child_lock_write),
*[e for i in range(MAX_BURNERS) for e in _burner_entities(i)],
),
)
@@ -123,8 +163,50 @@ COOKTOP_SAFETY = Capability(
poll_tier='warm',
entities=(
BinarySensorDesc(key='cooktop_safety_shutoff_enabled', field='safetyAlert',
name='Hot surface auto-shutoff enabled',
entity_category='config',
value_fn=lambda v: (v or {}).get('state') == 'on'),
),
)
# Bluetooth meat probe (issue #86). All-idle sentinel values when
# disconnected (operationBurnerNumber -1, temperatures 0) -- no special
# gating on those, matching cooktop.PAIRED_HOOD_STATUS's own precedent of
# showing a disconnected accessory's fields plainly rather than hiding the
# whole capability.
PROBE_STATUS = Capability(
href='/bluetooth/probe/status/vs/0',
poll_tier='warm',
entities=(
BinarySensorDesc(key='probe_connected', field='connectionState',
device_class='connectivity',
value_fn=lambda v: str(v).lower() == 'connected'),
SensorDesc(key='probe_battery', field='batteryPercentage',
device_class='battery', state_class='measurement',
unit='%', entity_category='diagnostic'),
SensorDesc(key='probe_temperature', field='currentTemperature',
device_class='temperature', state_class='measurement',
unit_fn=lambda rep: normalize_temp_unit(rep.get('temperatureUnit'), '°C')),
SensorDesc(key='probe_target_temperature', field='targetTemperature',
device_class='temperature', entity_category='diagnostic',
unit_fn=lambda rep: normalize_temp_unit(rep.get('temperatureUnit'), '°C')),
),
)
# Some range boards (issue #74's NE63B8411SS) report no /cooktop/status/vs/0
# burner array at all -- their local API only exposes this coarse
# monitoring resource for the cooktop half, with no per-burner detail.
# Meaning of `cooktopMonitoring` (a bare "0" on the only dump seen) and
# `warmingCenterState`'s full value set aren't confirmed, so both are
# exposed as plain sensors rather than guessed at as a switch/select --
# `supportedHoodLampStateList` has no corresponding live-state field on
# this resource, so nothing to bind it to yet.
COOKTOP_MONITORING = Capability(
href='/cooktopmonitoring/vs/0',
poll_tier='warm',
entities=(
SensorDesc(key='cooktop_running_state', field='x.com.samsung.da.cooktopRunningState',
icon='mdi:pot-steam-outline'),
SensorDesc(key='warming_center_state', field='x.com.samsung.da.warmingCenterState',
icon='mdi:heat-wave', entity_category='diagnostic'),
),
)
@@ -52,7 +52,6 @@ HOOD_ALARMS = Capability(
SensorDesc(
key='alarm_code',
field='x.com.samsung.da.items',
name='Alarm code',
icon='mdi:alert',
entity_category='diagnostic',
value_fn=_active_alarm_codes,
@@ -93,13 +92,11 @@ HOOD_FAN = Capability(
FanDesc(
key='fan',
field='x.com.samsung.da.hood.fanSpeed',
name=None,
write_fn=_hood_fan_write,
),
BinarySensorDesc(
key='automatic_operation',
field='x.com.samsung.da.hood.autoOperation',
name='Automatic operation',
icon='mdi:fan-auto',
entity_category='diagnostic',
value_fn=lambda value: str(value).lower() == 'on',
@@ -125,7 +122,6 @@ HOOD_LAMP = Capability(
SwitchDesc(
key='lamp',
field='x.com.samsung.lamp.power',
name='Lamp',
icon='mdi:range-hood',
value_fn=lambda value: str(value).lower() == 'on',
write_fn=lambda payload, rep, href=None: (
@@ -136,7 +132,6 @@ HOOD_LAMP = Capability(
SelectDesc(
key='lamp_brightness',
field='x.com.samsung.lamp.current',
name='Lamp brightness',
icon='mdi:brightness-6',
translation_key='range_hood_lamp_brightness',
options_field='x.com.samsung.lamp.range',
@@ -153,7 +148,6 @@ HOOD_FILTER = Capability(
SensorDesc(
key='hood_filter_usage',
field='x.com.samsung.da.filterUsage',
name='Filter usage',
unit='%',
state_class='measurement',
icon='mdi:air-filter',
@@ -163,14 +157,18 @@ HOOD_FILTER = Capability(
SensorDesc(
key='hood_filter_status',
field='x.com.samsung.da.filterStatus',
name='Filter status',
icon='mdi:air-filter',
entity_category='diagnostic',
device_class='enum',
options=('normal', 'wash', 'replace'),
translation_key='filter_status',
value_fn=lambda value: (
value.lower() if isinstance(value, str) else value
),
),
SensorDesc(
key='hood_filter_capacity',
field='x.com.samsung.da.filterCapacity',
name='Filter capacity',
unit='h',
icon='mdi:timer-outline',
entity_category='diagnostic',
@@ -188,26 +186,22 @@ AIR_QUALITY = Capability(
SensorDesc(
key='clean_level',
field='x.com.samsung.da.items',
name='Clean level',
icon='mdi:air-filter',
value_fn=lambda items: sensor_item_value(items, 'CleanLevel'),
),
SensorDesc(
key='dust',
field='x.com.samsung.da.items',
name='Dust',
value_fn=lambda items: sensor_item_value(items, 'Dust'),
),
SensorDesc(
key='fine_dust',
field='x.com.samsung.da.items',
name='Fine dust',
value_fn=lambda items: sensor_item_value(items, 'FineDust'),
),
SensorDesc(
key='super_fine_dust',
field='x.com.samsung.da.items',
name='Super fine dust',
value_fn=lambda items: sensor_item_value(items, 'SuperFineDust'),
),
),
@@ -221,7 +215,6 @@ AIR_LEVEL_CHECK = Capability(
BinarySensorDesc(
key='periodic_air_sensing',
field='x.com.samsung.da.periodicSensingActivationState',
name='Periodic air sensing',
icon='mdi:radar',
entity_category='diagnostic',
value_fn=lambda value: str(value).lower() == 'on',
@@ -229,14 +222,12 @@ AIR_LEVEL_CHECK = Capability(
SensorDesc(
key='air_sensing_state',
field='x.com.samsung.da.sensingState',
name='Air sensing state',
icon='mdi:radar',
entity_category='diagnostic',
),
SensorDesc(
key='last_air_sensing_time',
field='x.com.samsung.da.lastSensingTime',
name='Last air sensing time',
device_class='timestamp',
entity_category='diagnostic',
value_fn=_timestamp,
@@ -244,14 +235,12 @@ AIR_LEVEL_CHECK = Capability(
SensorDesc(
key='last_air_sensing_level',
field='x.com.samsung.da.lastSensingLevel',
name='Last air sensing level',
icon='mdi:air-filter',
entity_category='diagnostic',
),
SensorDesc(
key='automatic_ventilation_state',
field='x.com.samsung.da.autoExeState',
name='Automatic ventilation state',
icon='mdi:fan-auto',
entity_category='diagnostic',
),
@@ -266,7 +255,6 @@ AUTO_VENTILATION = Capability(
SensorDesc(
key='auto_ventilation_action',
field='action',
name='Auto ventilation action',
icon='mdi:fan-auto',
),
),
@@ -0,0 +1,109 @@
"""Capabilities for the Samsung stick-vacuum clean/auto-empty station
(model A-VSKR-TP1-22-VS9500AL, "Bespoke Jet" clean station, issue #131).
The reporter's diagnostics dump shows no vacuum-body state at all (no
suction level, no battery, no cleaning-mode/room-mapping control, no
docked/undocked status even) -- only the clean station's own dustbag,
dustbin auto-empty settings, and UV-C sanitizing-cycle status. This
strongly suggests the WiFi/DTLS module lives in the station, not the
handheld stick: the station is the only "device" this integration's local
API can see, and the wand's own controls are unrelated hardware not
reachable this way. Modeled as its own device type -- these hrefs don't
overlap with any existing family, so there's no shared-href ambiguity to
resolve against another type (see registry/by_type/__init__.py's
docstring for that rule).
Resources verified against the issue #131 diagnostics dump.
"""
from ..capability import Capability
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc, SwitchDesc
from .common import int_or_none, parse_iso_utc as _parse_iso_utc
DUSTBAG = Capability(
href='/component/station/dustbag/vs/0',
poll_tier='warm',
entities=(
BinarySensorDesc(key='dustbag_full', field='x.com.samsung.da.status',
device_class='problem',
icon='mdi:bag-personal',
value_fn=lambda v: v == 'full'),
),
)
# dustbagUsage/dustbagPrevUsage are raw counters with no capacity/resolution
# field alongside them to normalize into a percentage (unlike the AC/range
# families' filterUsage, which always ships filterCapacity) -- exposed as a
# plain diagnostic count rather than guessing a unit.
DUSTBAG_USAGE = Capability(
href='/component/station/dustbagusage/vs/0',
poll_tier='cold',
entities=(
SensorDesc(key='dustbag_usage', field='x.com.samsung.da.dustbagUsage',
icon='mdi:counter', entity_category='diagnostic',
state_class='total_increasing', value_fn=int_or_none),
),
)
DUSTBIN_SETTING = Capability(
href='/setting/dustbin/vs/0',
poll_tier='warm',
entities=(
SwitchDesc(key='auto_empty', field='x.com.samsung.da.autoEmpty',
icon='mdi:delete-empty',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
['setting', 'dustbin', 'vs', '0'],
{'x.com.samsung.da.autoEmpty': 'On' if p == 'On' else 'Off'})),
SwitchDesc(key='dustbin_auto_close', field='x.com.samsung.da.autoClose',
icon='mdi:door-sliding',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
['setting', 'dustbin', 'vs', '0'],
{'x.com.samsung.da.autoClose': 'On' if p == 'On' else 'Off'})),
SelectDesc(key='discharging_time', field='x.com.samsung.da.desiredDischargingTime',
icon='mdi:timer-outline',
entity_category='config',
options_field='x.com.samsung.da.supportedDischargingTime',
write_fn=lambda p, rep, href=None: (
['setting', 'dustbin', 'vs', '0'],
{'x.com.samsung.da.desiredDischargingTime': p})),
),
)
# stickStatus's exact meaning (docked? powered? charging?) isn't confirmed
# from this single On/Off dump -- exposed as a plain diagnostic sensor
# rather than a binary_sensor, so no polarity/semantic is asserted that
# might be wrong.
CLEANSTATION_STATUS = Capability(
href='/status/cleanstation/vs/0',
poll_tier='warm',
entities=(
SensorDesc(key='cleanstation_status', field='x.com.samsung.da.status',
icon='mdi:home-lightning-bolt', entity_category='diagnostic'),
SensorDesc(key='stick_status', field='x.com.samsung.da.stickStatus',
icon='mdi:broom', entity_category='diagnostic'),
SwitchDesc(key='uvc_intensive_mode', field='x.com.samsung.da.uvcIntensive',
icon='mdi:lightbulb-on-outline',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
['status', 'cleanstation', 'vs', '0'],
{'x.com.samsung.da.uvcIntensive': 'On' if p == 'On' else 'Off'})),
SensorDesc(key='uvc_operation_time', field='x.com.samsung.da.uvcOperationTime',
icon='mdi:timer-sand', entity_category='diagnostic',
value_fn=int_or_none),
SensorDesc(key='uvc_total_operation_time',
field='x.com.samsung.da.uvcTotalOperationTime',
icon='mdi:timer-sand', entity_category='diagnostic',
state_class='total_increasing', value_fn=int_or_none),
SensorDesc(key='uvc_finished_time', field='x.com.samsung.da.uvcFinishedTime',
device_class='timestamp', entity_category='diagnostic',
value_fn=_parse_iso_utc),
SensorDesc(key='uvc_emitted_time', field='x.com.samsung.da.emittedTime',
device_class='timestamp', entity_category='diagnostic',
value_fn=_parse_iso_utc),
),
)
@@ -23,8 +23,8 @@ from .laundry import (
)
# ---------------------------------------------------------------------------
# Course_XX hex codes. 23 of the codes named in strings.json/translations
# under entity.select.washer_cycle.state.<id, lowercased> were captured
# Course_XX hex codes. 23 of the codes named in translations/en.json
# under entity.select.washer_cycle_table_02.state.<id, lowercased> were captured
# from a live WW90DG6U25LEU4's x.com.samsung.da.editCourseList
# (EditCourseList_1C1D211B1E29243328262722202325322F2E30662D8F96), matched
# positionally against a Slovak-UI user's screenshots of their app's course
@@ -45,6 +45,14 @@ from .laundry import (
# (issue #22). Combo units carry their own course set, so these codes
# don't imply anything about '1F' on a plain washer.
#
# Three more -- '52' Eco Cold, '54' Towels, '60' Self Clean+ -- came from a
# WF50A8600AV/US, verified directly rather than by inference: the reporter
# selected each cycle on the physical appliance and read back the resulting
# raw code from the cycle_select entity's state (issue #80). '54' shares a
# display name with the existing '24' Towels -- a different code on a
# different course table legitimately landing on the same label, not a typo
# (same pattern as '21'/'65' Colors and '27'/'5E' Rinse+Spin above).
#
# No static fallback list of those codes is kept here, deliberately: other
# washer models have a different actual course set (a second dump's active
# course, '65', isn't even in the list above; models with 'AI Wash'/'Mixed
@@ -75,19 +83,19 @@ WASHER_SETTINGS = Capability(
href='/washer/vs/0',
entities=(
SelectDesc(key='wash_temperature', field='x.com.samsung.da.waterTemperature',
name='Wash temperature', icon='mdi:thermometer-water',
icon='mdi:thermometer-water',
entity_category='config',
options_field='x.com.samsung.da.supportedWaterTemperature',
write_fn=lambda p, rep, href=None: (
['washer', 'vs', '0'], {'x.com.samsung.da.waterTemperature': p})),
SelectDesc(key='spin_speed', field='x.com.samsung.da.spinLevel',
name='Spin speed', icon='mdi:sync',
icon='mdi:sync',
entity_category='config',
options_field='x.com.samsung.da.supportedSpinLevel',
write_fn=lambda p, rep, href=None: (
['washer', 'vs', '0'], {'x.com.samsung.da.spinLevel': p})),
SelectDesc(key='rinse_cycles', field='x.com.samsung.da.rinseCycles',
name='Rinse cycles', icon='mdi:water-sync',
icon='mdi:water-sync',
entity_category='config',
options_field='x.com.samsung.da.supportedRinseCycles',
write_fn=lambda p, rep, href=None: (
@@ -97,7 +105,7 @@ WASHER_SETTINGS = Capability(
# #22. Self-gates off on plain washers, which never report
# supportedDryLevel.
SelectDesc(key='dry_level', field='x.com.samsung.da.dryLevel',
name='Dry level', icon='mdi:tumble-dryer',
icon='mdi:tumble-dryer',
entity_category='config',
translation_key='washer_dry_level',
options_field='x.com.samsung.da.supportedDryLevel',
@@ -157,8 +165,8 @@ def _drum_clean_last_cleaned(rep):
# valid raw codes for its field, same hex-pair shape as EditCourseList.
# '<Prefix>Alarm_<On/Off>' is a low-reservoir warning flag.
#
# Label mapping (entity.select.washer_dosing_quantity/washer_detergent_
# water_hardness/washer_softener_concentration in strings.json) is an
# Label mapping (entity.select.{detergent,softener}_quantity /
# detergent_water_hardness / softener_concentration in translations/en.json) is an
# assumed, not cross-device-verified, reading of the single issue #9 dump +
# screenshots: LevelCtrl's 4 codes as None/Low/Medium/High (00 has no
# on-screen equivalent -- the app's Quantité picker only offers
@@ -259,7 +267,7 @@ def _dosing_low(prefix):
# auto-release-dry toggles -- only this per-course gating is washer-only, so
# it stays here rather than in laundry.py (see laundry.bool_option_switch's
# docstring: it takes a prebuilt validate_fn and has no opinion on it).
def _bool_option_switch(key, name, icon, prefix, availability_field):
def _bool_option_switch(key, icon, prefix, availability_field):
def validate(p, rep, resources):
"""Reject turning on when the selected course's byte in
`availability_field` isn't 'F0'. Turning off is never blocked. Falls
@@ -281,11 +289,11 @@ def _bool_option_switch(key, name, icon, prefix, availability_field):
if len(pairs) != len(courses):
return None
if pairs[courses.index(current)] != 'F0':
return f"{name} isn't available on the selected cycle."
return f"{key}_unavailable_for_cycle"
return None
return bool_option_switch(
key, name, icon, prefix,
key, icon, prefix,
entity_category='config', gate_on_presence=True, validate_fn=validate)
@@ -294,63 +302,61 @@ WASHER_COURSE = Capability(
entities=(
cycle_select(translation_key='washer_cycle', icon='mdi:washing-machine',
table_href='/st/washercourse/vs/0'),
SensorDesc(key='drum_clean_cycles_remaining', name='Drum clean due in',
icon='mdi:washing-machine-alert', unit='cycles',
SensorDesc(key='drum_clean_cycles_remaining', unit='cycles',
icon='mdi:washing-machine-alert',
state_class='measurement',
exists_fn=lambda rep, resources: _drum_clean_cycles_remaining(rep) is not None,
rep_fn=_drum_clean_cycles_remaining),
SensorDesc(key='drum_clean_last_cleaned', name='Drum last cleaned',
icon='mdi:calendar-clock', device_class='timestamp',
SensorDesc(key='drum_clean_last_cleaned', device_class='timestamp',
icon='mdi:calendar-clock',
entity_category='diagnostic',
exists_fn=lambda rep, resources: _drum_clean_last_cleaned(rep) is not None,
rep_fn=_drum_clean_last_cleaned),
SelectDesc(key='detergent_quantity', name='Detergent quantity', icon='mdi:cup-water',
translation_key='washer_dosing_quantity',
SelectDesc(key='detergent_quantity', icon='mdi:cup-water',
translation_key='detergent_quantity',
entity_category='config',
options=_level_options('DetergentLevelCtrl'),
exists_fn=lambda rep, resources: bool(
_level_options('DetergentLevelCtrl')(resources)),
rep_fn=_dosing_level('DetergentLevelCtrl'),
write_fn=_level_write('DetergentLevelCtrl')),
SelectDesc(key='detergent_water_hardness', name='Detergent water hardness',
icon='mdi:water-opacity',
translation_key='washer_detergent_water_hardness',
SelectDesc(key='detergent_water_hardness', icon='mdi:water-opacity',
translation_key='detergent_water_hardness',
entity_category='config',
options=_level_options('DetergentLevel2Ctrl'),
exists_fn=lambda rep, resources: bool(
_level_options('DetergentLevel2Ctrl')(resources)),
rep_fn=_dosing_level('DetergentLevel2Ctrl'),
write_fn=_level_write('DetergentLevel2Ctrl')),
SelectDesc(key='softener_quantity', name='Softener quantity', icon='mdi:flask-outline',
translation_key='washer_dosing_quantity',
SelectDesc(key='softener_quantity', icon='mdi:flask-outline',
translation_key='softener_quantity',
entity_category='config',
options=_level_options('SoftenerLevelCtrl'),
exists_fn=lambda rep, resources: bool(
_level_options('SoftenerLevelCtrl')(resources)),
rep_fn=_dosing_level('SoftenerLevelCtrl'),
write_fn=_level_write('SoftenerLevelCtrl')),
SelectDesc(key='softener_concentration', name='Softener concentration',
icon='mdi:flask-plus-outline',
translation_key='washer_softener_concentration',
SelectDesc(key='softener_concentration', icon='mdi:flask-plus-outline',
translation_key='softener_concentration',
entity_category='config',
options=_level_options('SoftenerLevel2Ctrl'),
exists_fn=lambda rep, resources: bool(
_level_options('SoftenerLevel2Ctrl')(resources)),
rep_fn=_dosing_level('SoftenerLevel2Ctrl'),
write_fn=_level_write('SoftenerLevel2Ctrl')),
BinarySensorDesc(key='detergent_low', name='Detergent low',
icon='mdi:alert-circle-outline', device_class='problem',
BinarySensorDesc(key='detergent_low', device_class='problem',
icon='mdi:alert-circle-outline',
exists_fn=bool_option_exists('DetergentAlarm'),
rep_fn=_dosing_low('DetergentAlarm')),
BinarySensorDesc(key='softener_low', name='Softener low',
icon='mdi:alert-circle-outline', device_class='problem',
BinarySensorDesc(key='softener_low', device_class='problem',
icon='mdi:alert-circle-outline',
exists_fn=bool_option_exists('SoftenerAlarm'),
rep_fn=_dosing_low('SoftenerAlarm')),
_bool_option_switch('bubble_soak', 'Bubble soak', 'mdi:chart-bubble',
_bool_option_switch('bubble_soak', 'mdi:chart-bubble',
'BubbleSoak', 'BubbleSoakSet'),
_bool_option_switch('pre_wash', 'Pre wash', 'mdi:washing-machine',
_bool_option_switch('pre_wash', 'mdi:washing-machine',
'PreWashSetting', 'PreWashAvailableSet'),
_bool_option_switch('intensive', 'Intensive', 'mdi:washing-machine',
_bool_option_switch('intensive', 'mdi:washing-machine',
'IntensiveSetting', 'IntensiveAvailableSet'),
),
)
@@ -0,0 +1,203 @@
"""Capabilities for the Samsung water-purifier family (TP2X_WATERPURIFIER-class,
issue #90, model TP2X_WATERPURIFIER_20K).
Resources verified against the issue #90 diagnostics dump.
"""
from ..capability import Capability
from ..entities import BinarySensorDesc, NumberDesc, SelectDesc, SensorDesc, SwitchDesc
from .common import int_or_none, parse_iso_utc as _parse_iso_utc
DISPENSE = Capability(
href='/setting/waterpurifier/vs/0',
poll_tier='warm',
entities=(
SelectDesc(key='dispense_type', field='x.com.samsung.da.desiredType',
icon='mdi:cup-water',
options_field='x.com.samsung.da.supportedTypes',
write_fn=lambda p, rep, href=None: (
['setting', 'waterpurifier', 'vs', '0'],
{'x.com.samsung.da.desiredType': p})),
# Only a handful of discrete temperatures are selectable (not a
# continuous range) -- a select over the live-reported set, not a
# number with invented bounds.
SelectDesc(key='hot_water_temperature', field='x.com.samsung.da.tempDesiredHotWater',
icon='mdi:thermometer',
entity_category='config',
options_field='x.com.samsung.da.supportedHotTemperatures',
write_fn=lambda p, rep, href=None: (
['setting', 'waterpurifier', 'vs', '0'],
{'x.com.samsung.da.tempDesiredHotWater': p})),
# Bounds and step come live from the device's own
# desiredCapacityRange/capacityResolution fields, not a hardcoded
# constant -- see the adding-device-support skill's "never hard-code
# the one dump's values" section. No unit is set: capacityUnit reads
# "C" on this dump, which can't be right for a volume field, so per
# the 'don't guess' rule the unit is left unset rather than assumed
# to be mL.
NumberDesc(key='dispense_capacity', field='x.com.samsung.da.desiredCapacity',
icon='mdi:cup-water',
value_fn=int_or_none,
range_field='x.com.samsung.da.desiredCapacityRange',
step_fn=lambda rep: int_or_none(
rep.get('x.com.samsung.da.capacityResolution')) or 1,
write_fn=lambda p, rep, href=None: (
['setting', 'waterpurifier', 'vs', '0'],
{'x.com.samsung.da.desiredCapacity': str(int(round(float(p))))})),
BinarySensorDesc(key='pouring', field='x.com.samsung.da.pourStatus',
icon='mdi:cup-water',
value_fn=lambda v: v == 'On'),
),
)
STATUS = Capability(
href='/status/waterpurifier/vs/0',
poll_tier='warm',
entities=(
SensorDesc(key='waterpurifier_status', field='x.com.samsung.da.status',
icon='mdi:water-pump',
entity_category='diagnostic'),
BinarySensorDesc(key='filter_door_status', field='x.com.samsung.da.filterDoorStatus',
device_class='door',
entity_category='diagnostic',
value_fn=lambda v: v == 'Open'),
SensorDesc(key='sterilize_period', field='x.com.samsung.da.sterilizePeriod',
icon='mdi:calendar-sync',
entity_category='diagnostic'),
SensorDesc(key='sterilize_run_time', field='x.com.samsung.da.sterilizeRunTime',
icon='mdi:timer-outline',
entity_category='diagnostic'),
SensorDesc(key='sterilize_last_time', device_class='timestamp',
entity_category='diagnostic',
rep_fn=lambda rep: _parse_iso_utc(
rep.get('x.com.samsung.da.sterilizeLastTime'))),
SensorDesc(key='sterilize_plan_time', device_class='timestamp',
entity_category='diagnostic',
rep_fn=lambda rep: _parse_iso_utc(
rep.get('x.com.samsung.da.sterilizePlanTime'))),
SensorDesc(key='filter_clean_remain_time', field='x.com.samsung.da.filterCleanRemainTime',
icon='mdi:timer-sand',
entity_category='diagnostic'),
),
)
FAVORITE_CAPACITY = Capability(
href='/favorite/capacity/vs/0',
poll_tier='cold',
entities=(
SwitchDesc(key='favorite_capacity_enabled', field='x.com.samsung.da.switchCapacity',
icon='mdi:star-outline',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
['favorite', 'capacity', 'vs', '0'],
{'x.com.samsung.da.switchCapacity': 'On' if p == 'On' else 'Off'})),
SelectDesc(key='favorite_capacity', field='x.com.samsung.da.defaultCapacity',
icon='mdi:cup-water',
entity_category='config',
options_field='x.com.samsung.da.capacityList',
write_fn=lambda p, rep, href=None: (
['favorite', 'capacity', 'vs', '0'],
{'x.com.samsung.da.defaultCapacity': p})),
),
)
# Coffee-capable variant (issue #107) -- same "favorite" enable-toggle +
# supported-list select shape as FAVORITE_CAPACITY above, but for the hot
# water dispensed alongside brewing rather than the pour capacity.
FAVORITE_HOTWATER = Capability(
href='/favorite/hotwater/vs/0',
poll_tier='cold',
entities=(
SwitchDesc(key='favorite_hotwater_enabled', field='x.com.samsung.da.switchHotwater',
icon='mdi:star-outline',
entity_category='config',
value_fn=lambda v: v != 'Locked',
write_fn=lambda p, rep, href=None: (
['favorite', 'hotwater', 'vs', '0'],
{'x.com.samsung.da.switchHotwater': 'Unlocked' if p == 'On' else 'Locked'})),
SelectDesc(key='favorite_hotwater_temperature',
field='x.com.samsung.da.favorite.defaultTemperature',
icon='mdi:thermometer',
entity_category='config',
options_field='x.com.samsung.da.favorite.supportedList',
write_fn=lambda p, rep, href=None: (
['favorite', 'hotwater', 'vs', '0'],
{'x.com.samsung.da.favorite.defaultTemperature': p})),
),
)
# Coffee-capable variant (issue #107). No 'x.com.samsung.da.' field prefix
# on this resource, unlike the rest of the water-purifier surface.
COFFEE = Capability(
href='/favorite/coffee/vs/0',
poll_tier='warm',
entities=(
SwitchDesc(key='favorite_coffee_enabled', field='favorite.activate',
icon='mdi:coffee-outline',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
['favorite', 'coffee', 'vs', '0'],
{'favorite.activate': 'On' if p == 'On' else 'Off'})),
SensorDesc(key='coffee_brew_status', field='brew.status',
icon='mdi:coffee-outline',
entity_category='diagnostic'),
),
)
LOCK = Capability(
href='/status/lock/vs/0',
poll_tier='warm',
entities=(
SwitchDesc(key='hotwater_lock', field='x.com.samsung.da.hotwaterLock',
device_class='lock',
entity_category='config',
value_fn=lambda v: v != 'Unlocked',
write_fn=lambda p, rep, href=None: (
['status', 'lock', 'vs', '0'],
{'x.com.samsung.da.hotwaterLock': 'Locked' if p == 'On' else 'Unlocked'})),
SwitchDesc(key='coldwater_lock', field='x.com.samsung.da.coldwaterLock',
device_class='lock',
entity_category='config',
value_fn=lambda v: v != 'Unlocked',
write_fn=lambda p, rep, href=None: (
['status', 'lock', 'vs', '0'],
{'x.com.samsung.da.coldwaterLock': 'Locked' if p == 'On' else 'Unlocked'})),
SwitchDesc(key='buzz_lock', field='x.com.samsung.da.buzzLock',
device_class='lock',
entity_category='config',
value_fn=lambda v: v != 'Unlocked',
write_fn=lambda p, rep, href=None: (
['status', 'lock', 'vs', '0'],
{'x.com.samsung.da.buzzLock': 'Locked' if p == 'On' else 'Unlocked'})),
),
)
# ---------------------------------------------------------------------------
# Water-purifier-scoped coverage: hrefs with no user-actionable state or no
# confirmed contract, following the 'don't guess' rule.
# ---------------------------------------------------------------------------
_WP_IGNORED = [
# supportedModes carries a single opaque wizard-workflow token
# ('HOMECARE_WIZARD_V2') and modes reports a completely different,
# unrelated value ('WATERFILTER_DISABLE') not even present in
# supportedModes -- internal plumbing, not a real user-facing mode
# select. OCF-standard /mode/0 mirrors the same vendor resource but is
# already covered by the global ignored.IGNORED (fridge's OCF-native
# vacation-mode flag shares that href).
'/mode/vs/0',
# Static support-flags blob (automation.supported.modes/options) -- no
# live "current automation setting" field to expose.
'/automation/waterpurifier/vs/0',
# Coffee-capable variant (issue #107). All four are static
# capability-advertisement blobs or empty -- no live "current recipe" /
# "current custom slot" field to expose, unlike /favorite/coffee/vs/0
# (COFFEE above), which does carry live brew status.
'/brand/recipe/info/vs/0', # revision + max-brand-count metadata
'/coffee/custom/recipe/vs/0', # publisher.support: allowed custom-recipe slot IDs
'/recipe/coffee/vs/0', # same publisher.support shape, no per-recipe content
'/recipe/coffee/deletion/vs/0', # empty {} on this dump
]
COVERAGE = [Capability(href=h) for h in _WP_IGNORED]
@@ -32,7 +32,7 @@ class BoundEntity:
def _snake_to_title(s: str) -> str:
"""'CUBED_ICE'/'cubed_ice' -> 'Cubed Ice'. Shared with entity.py's
_derive_name, which applies the same transform to a state key."""
_derive_name, which applies the same transform to an href-derived key."""
return s.replace('_', ' ').title()
@@ -8,14 +8,14 @@ where a description declares one.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Callable, Optional
from dataclasses import dataclass
from typing import Any, Callable, Mapping, Optional
WriteFn = Optional[Callable[[Any, dict], "tuple[list[str], dict] | None"]]
# (payload, rep, resources) -> a human-readable rejection message, or None to
# allow the write. resources is the coordinator's full href->rep snapshot, for
# the same cross-resource lookups exists_fn needs (e.g. reading a sibling
# href's live option list).
# (payload, rep, resources) -> a translation key, or None to allow the
# write. resources is the coordinator's full href->rep snapshot, for the same
# cross-resource lookups exists_fn needs (e.g. reading a sibling href's live
# option list).
ValidateFn = Optional[Callable[[Any, dict, dict], "str | None"]]
@@ -27,13 +27,18 @@ def _identity(v: Any) -> Any:
class SamsungEntityDescription:
key: str
field: str = ''
name: Optional[str] = None
# Defaults to `key`: entity names and states live in translations/, never
# here, so a descriptor only sets this to share one catalog entry across
# several descriptors, or to point at a differently-named one.
translation_key: Any = None # str | Callable[[dict[str, dict]], Optional[str]]
# callable form receives the coordinator's full href->rep resource
# snapshot and returns the key to use (or None for no translation this
# device) -- for a descriptor shared across board generations whose
# state-code meaning isn't guaranteed consistent between them; see
# laundry.cycle_select's table-id-gated resolver.
# snapshot and returns the key to use -- for a descriptor shared across
# board generations whose state-code meaning isn't guaranteed consistent
# between them; see laundry.cycle_select's table-id-gated resolver.
translation_placeholders: Optional[Mapping[str, str]] = None
# Dynamic resources such as fridge compartments and ice makers use a
# device-provided or href-derived instance label inside a translated name.
use_instance_name: bool = False
icon: Optional[str] = None
entity_category: Optional[str] = None # 'diagnostic' | 'config' | None
enabled_default: bool = True
+29 -4
View File
@@ -11,6 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .registry.entities import SelectDesc
from .catalog import translated_states
from .const import DOMAIN
from .coordinator import LocalThingsCoordinator
from .entity import LocalThingsEntity, _is_included
@@ -32,6 +33,22 @@ async def async_setup_entry(
_CAMEL_BOUNDARY_RE = re.compile(r'(?<=[a-z0-9])(?=[A-Z])')
def _translation_state(value: str, known: frozenset[str]) -> str | None:
"""Return the catalog state `value` normalizes to, else None.
Samsung reports options in whatever casing the resource uses
('Rinse_Hold', 'SpTtypeBeerDrinks', '1b'); Home Assistant looks state
translations up by a lowercase key. Only values the catalog actually
knows are normalized -- an unrecognized (or future) vendor value keeps
its own readable form rather than becoming an untranslatable slug.
"""
direct = value.lower().replace(' ', '_')
if direct in known:
return direct
snake = _CAMEL_BOUNDARY_RE.sub('_', value).lower().replace(' ', '_')
return snake if snake in known else None
def _display(value, translation_key: Optional[str]):
"""Turn a raw device option/state value into what's shown in the UI.
@@ -40,13 +57,13 @@ def _display(value, translation_key: Optional[str]):
callers pass the resolved value, e.g. self.translation_key, not
the raw descriptor field).
An entity with a translation_key looks its state up in strings.json,
and hassfest requires those keys to be lowercase -- so those values
An entity with a translation_key looks its state up in the shipped
translation catalog, whose state keys are lowercase -- so those values
must be lowercased exactly to match, and the device still expects
that same raw casing back on write (callers map the displayed value
back to raw via _raw_options()).
Everything else has no strings.json lookup, so there's no reason to
Everything else has no catalog lookup, so there's no reason to
destroy the device's own casing. Only two cosmetic fixups apply: a
fully lowercase device-native token (e.g. "voice") is title-cased,
and a PascalCase token (e.g. "ExtraHigh") gets a space inserted at
@@ -57,7 +74,15 @@ def _display(value, translation_key: Optional[str]):
if not isinstance(value, str):
return value
if translation_key:
return value.lower()
known = translated_states('select', translation_key)
if not known:
# No state table for this key: either the entity isn't translated
# at all, or its name is translated but its options deliberately
# aren't (an unrecognized course table, say). Either way the
# opaque device value is the best thing to show.
return value
if translated := _translation_state(value, known):
return translated
if value.islower():
return value.replace('_', ' ').title()
return _CAMEL_BOUNDARY_RE.sub(' ', value)
-1
View File
@@ -61,7 +61,6 @@ class LocalThingsConnectionModeSensor(CoordinatorEntity[LocalThingsCoordinator],
Disabled by default — it's for troubleshooting, not everyday use."""
_attr_has_entity_name = True
_attr_name = 'Connection mode'
_attr_translation_key = 'connection_mode'
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_entity_registry_enabled_default = False
-302
View File
@@ -1,302 +0,0 @@
{
"entity": {
"select": {
"door_alert": {
"state": {
"1": "Alarm 1",
"2": "Alarm 2",
"3": "Alarm 3",
"4": "Alarm 4"
}
},
"brightness_level": {
"state": {
"33": "Low",
"66": "Medium",
"100": "High"
}
},
"range_hood_lamp_brightness": {
"state": {
"1": "Level 1",
"2": "Level 2"
}
},
"ice_type": {
"state": {
"off": "Off",
"whiskey_iceball_3": "3 Balls/Day",
"whiskey_iceball_6": "6 Balls/Day",
"whiskey_iceball_9": "9 Balls/Day"
}
},
"beverage_zone_mode": {
"state": {
"sp_ttype_beer_drinks": "Beverage",
"sp_ttype_wine_dessert": "Wine and Dessert"
}
},
"pantry_zone_mode": {
"state": {
"fdr_wine": "Wine",
"fdr_deli": "Deli",
"fdr_drinks": "Drinks"
}
},
"flex_zone_mode": {
"state": {
"cv_ttype_rf9000a_freeze": "Freeze",
"cv_ttype_rf9000a_softfreeze": "Soft Freeze",
"cv_ttype_rf9000a_meat_fish": "Meat/Fish",
"cv_ttype_rf9000a_fruit_veggies": "Fruit & Veggies",
"cv_ttype_rf9000a_beverage": "Beverage",
"cv_fdr_wine": "Wine",
"cv_fdr_deli": "Deli",
"cv_fdr_beverage": "Beverage",
"cv_fdr_meat": "Meat",
"cv_fdr_soft_freezer": "Soft Freezer"
}
},
"dishwasher_cycle": {
"state": {
"0e": "AI Wash",
"07": "Pre blast",
"90": "Self clean",
"86": "Normal",
"83": "Express 60",
"84": "Heavy",
"8d": "Pots and pans",
"80": "Delicate",
"8e": "Plastic",
"8f": "Baby Care"
}
},
"washer_cycle_table_02": {
"state": {
"1c": "Eco 40-60",
"1d": "Super Speed",
"21": "Colours",
"1b": "Cotton",
"1e": "15' Quick Wash",
"29": "Drum Clean+",
"24": "Towels",
"33": "Bedding",
"28": "Drain/Spin",
"26": "Delicates",
"27": "Rinse+Spin",
"22": "Wool",
"20": "Hygiene Steam",
"23": "Outdoor",
"25": "Synthetics",
"32": "Shirts",
"2f": "Activewear",
"2e": "Baby Care",
"30": "Cloudy Day",
"66": "Denim",
"2d": "Silent Wash",
"8f": "Intense Cold",
"96": "Less Microfiber",
"36": "Wash+Dry",
"37": "Air Wash",
"38": "Cotton Dry",
"39": "Synthetics Dry",
"1f": "Intense Cold"
}
},
"dryer_cycle_table_03": {
"state": {
"16": "Cotton",
"18": "Synthetics",
"19": "Delicates",
"1a": "Wool",
"1b": "Bedding",
"1c": "Shirts",
"1d": "Towels",
"1e": "Outdoor",
"1f": "Mixed Load",
"20": "Iron Dry",
"23": "Quick Dry 35",
"24": "Cool Air",
"25": "Warm Air",
"27": "Time Dry"
}
},
"washer_dosing_quantity": {
"state": {
"00": "None",
"01": "Low",
"02": "Medium",
"03": "High"
}
},
"washer_detergent_water_hardness": {
"state": {
"01": "Soft",
"02": "Medium",
"03": "Hard"
}
},
"washer_softener_concentration": {
"state": {
"01": "1x",
"02": "2x",
"03": "3x"
}
},
"washer_dry_level": {
"state": {
"none": "Off",
"cupboard": "Cupboard",
"30": "30 min",
"60": "1 hr",
"90": "1 hr 30",
"120": "2 hr",
"180": "3 hr",
"240": "4 hr"
}
}
},
"sensor": {
"ice_making_status": {
"state": {
"icestatus_stop": "Idle",
"icestatus_run": "Making ice"
}
},
"connection_mode": {
"state": {
"observe": "Push (observe)",
"poll": "Polling"
}
},
"machine_state": {
"state": {
"idle": "Idle",
"active": "Active",
"pause": "Paused"
}
},
"air_filter_status": {
"state": {
"normal": "Normal",
"wash": "Wash",
"replace": "Replace"
}
}
},
"climate": {
"airconditioner": {
"state_attributes": {
"fan_mode": {
"state": {
"auto": "Auto",
"low": "Low",
"medium": "Medium",
"high": "High",
"turbo": "Turbo"
}
},
"swing_mode": {
"state": {
"off": "Fixed",
"both": "All directions",
"vertical": "Up and down"
}
},
"preset_mode": {
"state": {
"none": "Off",
"sleep": "Sleep",
"quiet": "Quiet",
"smart": "Smart",
"speed": "Speed"
}
}
}
}
}
},
"config": {
"step": {
"user": {
"title": "Add Samsung Appliance",
"description": "Enter the IP address of your Samsung appliance and the AC14K_M CA credentials used to mint device certificates. {reuse_note}",
"data": {
"host": "IP Address",
"ca_cert_pem": "CA Certificate (PEM)",
"ca_key_pem": "CA Private Key (PEM)"
},
"data_description": {
"host": "Local IP address of your Samsung appliance (e.g. 192.168.1.50).",
"ca_cert_pem": "The PEM-encoded AC14K_M CA certificate chain. Paste the full contents of your fullchain PEM including all BEGIN/END CERTIFICATE blocks.",
"ca_key_pem": "The PEM-encoded AC14K_M private key. Paste the full contents including the BEGIN/END PRIVATE KEY header and footer."
}
},
"confirm_unknown_type": {
"title": "Appliance type not recognized",
"description": "This appliance reported oneUiVersion \"{one_ui_version}\", which isn't a recognized type. It'll still be added, but only with common capabilities (power, alarms, etc. where present) rather than the full set for its family. You can help add full support afterward by downloading diagnostics for this device (Settings > Devices & Services > this device > the menu > Download diagnostics) and filing them in a new issue. Submit to add it anyway."
}
},
"error": {
"cannot_connect": "Cannot connect to the device. Verify the IP address is reachable and the CA credentials are correct.",
"invalid_ca": "The CA certificate or private key could not be loaded. Verify the PEM contents are correct and the key matches the certificate.",
"unknown": "Unexpected error. Check the Home Assistant logs for details."
},
"abort": {
"already_configured": "This device is already configured."
}
},
"options": {
"step": {
"init": {
"title": "Local Things Options",
"menu_options": {
"settings": "Remote control write settings",
"debug_write": "Debug: write to a resource"
}
},
"settings": {
"title": "Remote control write settings",
"description": "Some devices accept certain writes (e.g. default detergent/softener dosing on a washer) even while reporting remote control off. By default, LocalThings blocks every write with a clear error whenever a device reports remote control off, rather than letting the device silently reject it. Only enable this if you've confirmed writes actually work on this device with remote control off -- otherwise you'll trade that clear error for a silent failure.",
"data": {
"bypass_remote_control_lock": "Allow writes even when remote control is reported off"
}
},
"debug_write": {
"title": "Debug: write to a resource",
"description": "Power-user tool for pinning down device-specific write behavior. Pick the resource (href) you want to write to, or type a custom one that isn't listed. This bypasses the remote-control-off block and sends exactly the fields you provide -- it can misconfigure your appliance, so use it deliberately.",
"data": {
"href": "Resource href"
}
},
"debug_edit": {
"title": "Debug write: {href}",
"description": "Current value of `{href}`:\n```\n{current_value}\n```\nEnter ONLY the field(s) you want to change below. What you enter is sent to the device as-is (a partial update); it is NOT merged with the fields shown above, so don't paste the whole value back. Mind the types: a numeric string like \"1\" must stay quoted -- a bare 1 is sent as an integer, which some devices reject.",
"data": {
"payload": "Payload to write"
}
},
"debug_result": {
"title": "Debug write result",
"description": "The device returned CoAP code {code}. A 2.xx class means the write was accepted; 4.xx/5.xx means it was rejected. The resource now reads:\n```\n{new_value}\n```\nIf the value is unchanged, the device likely dropped the write. Pick what to do next:",
"menu_options": {
"debug_write": "Write another resource",
"finish": "Finish"
}
}
},
"error": {
"empty_payload": "Enter at least one field to write.",
"write_failed": "The write failed. Check the Home Assistant logs for details."
},
"abort": {
"not_loaded": "This device isn't connected yet. Try again once it has loaded."
}
},
"issues": {
"device_gap": {
"title": "Incomplete capability coverage for {device_name}",
"description": "This device is missing full capability coverage. Either its appliance type wasn't recognized, or some of the resources it exposes aren't modeled yet. It'll keep working with whatever is already supported. You can help expand support by going to Settings > Devices & Services > {device_name} > the menu (top right) > Download diagnostics, then filing it with the linked issue template."
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+466
View File
@@ -0,0 +1,466 @@
{
"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.sensingState": "NonProcessing",
"x.com.samsung.da.lastSensingTime": "1785157841",
"x.com.samsung.da.lastSensingLevel": "Kr1",
"x.com.samsung.da.periodicSensingSkipStatus": "Off",
"x.com.samsung.da.periodicSensingSkipTime": "00000000",
"x.com.samsung.da.periodicSensingActivationState": "On",
"x.com.samsung.da.autoExeState": "Off",
"x.com.samsung.da.supportedAutoExeState": [
"Off",
"Airpurify",
"Alarm"
],
"x.com.samsung.da.startSensingOnce": "Off"
}
},
{
"href": "/alarms/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "ErrorCode_OFF",
"x.com.samsung.da.triggeredTime": "2026-07-27T13:18:51",
"x.com.samsung.da.state": "Deleted"
}
],
"rt": [
"x.com.samsung.da.alarms"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "3B000100000A0000000000000000",
"x.com.samsung.da.id": "VTL",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "2650000000",
"x.com.samsung.da.airconOptionList": [
"AI_PURIFY",
"WELCOMECARE",
"DR"
]
}
},
{
"href": "/da/softreset/vs/0",
"rep": {
"x.com.samsung.da.softwarereset": "false"
}
},
{
"href": "/devicespecificinfo/vs/0",
"rep": {
"x.com.samsung.da.deviceActive": true
}
},
{
"href": "/display/vs/0",
"rep": {
"mode": "On",
"supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/dnd/autosleep/vs/0",
"rep": {
"x.com.samsung.da.visible": "true",
"x.com.samsung.da.startTime": "00:00:00",
"x.com.samsung.da.endTime": "00:00:00",
"x.com.samsung.da.useTimeSetting": "false",
"x.com.samsung.da.functionState": "false"
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.start": "1970-01-01T00:00:00Z",
"x.com.samsung.da.override": "Off",
"x.com.samsung.da.durationminutes": "0",
"x.com.samsung.da.drlcLevel": "0",
"x.com.samsung.da.realSaving": "Off"
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativeDate": "1785158301",
"x.com.samsung.da.cumulativeSavedPower": "0",
"x.com.samsung.da.cumulativePower": "32428",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePowerType": "total"
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+09:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/filter/hepafilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "70",
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterCapacity": "8760",
"x.com.samsung.da.filterCapacityUnit": "Hour",
"x.com.samsung.da.filterResetType": [
"replaceable"
],
"x.com.samsung.da.filterStatus": "normal"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.modelNum": "TP1X_DA-AC-AIR-01031_0000|10268241|70000721001813CC4D1A490301082100",
"x.com.samsung.da.description": "TP1X_DA-AC-AIR-01031_0000",
"x.com.samsung.da.serialNum": "**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": "02660A260401",
"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.newVersionAvailable": "0",
"x.com.samsung.da.number": "02682A25042102,02683A25031801"
}
],
"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": "AP2",
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01"
}
},
{
"href": "/keepnormalstate/vs/0",
"rep": {
"x.com.samsung.da.keepnormal": 1
}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/light/vs/0",
"rep": {
"mode": "On",
"supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.modes": [
"Smart"
],
"x.com.samsung.da.supportedModes": [
"Smart",
"Max",
"Mid",
"WindFree",
"Sleep"
],
"x.com.samsung.da.options": [
"OptionCode_23418",
"SmartSleep_Off",
"WelcomeCare_On",
"Pollution_Off",
"ProtectOperation_Off",
"UpdateAllow_NotAllowed"
],
"rt": [
"x.com.samsung.da.mode"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/option/muteonce/vs/0",
"rep": {
"muteonce": "Off"
}
},
{
"href": "/panel/vs/0",
"rep": {
"status": "Close"
}
},
{
"href": "/personality/presence/vs/0",
"rep": {}
},
{
"href": "/petfilteractivation/vs/0",
"rep": {
"status": "Off"
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "On",
"operationNumber": "36",
"displayCondition": "Enable"
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.longnotisubscription": "true",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/reserverulesets/vs/0",
"rep": {
"x.com.samsung.da.sets": "3F00000000000000000000",
"x.com.samsung.da.id": "VTL",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/sensors/vs/0",
"rep": {
"x.com.samsung.da.cleanLevel": "1",
"x.com.samsung.da.total": "5",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"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": "1",
"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": "2",
"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": "3",
"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": "4",
"x.com.samsung.da.description": "Sensor for SuperFineDust",
"x.com.samsung.da.type": "SuperFineDust",
"x.com.samsung.da.value": [
"5",
"1"
]
}
],
"rt": [
"x.com.samsung.da.sensors"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/settings/sound/mode/vs/0",
"rep": {
"mode": "mute",
"supportedModes": [
"mute",
"buzzer"
]
}
},
{
"href": "/settings/sound/output/vs/0",
"rep": {
"deviceType": "buzzer"
}
},
{
"href": "/settings/sound/volume/vs/0",
"rep": {
"level": "0",
"minLevel": "0",
"maxLevel": "3",
"resolution": "1"
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "Micom",
"x.com.samsung.da.newVersionAvailable": "false",
"x.com.samsung.da.newVersionNo": "12250421",
"x.com.samsung.da.currentVersionInfo": "10000000",
"otnStatus": "None",
"flashingProgress": "0",
"otnTarget": "main",
"otnCompleteDate": "noHistory",
"scheduledTime": "None",
"swVersionInfo": {
"platform": "Tizen Lite",
"oneUiVersion": "7.0 Air purifier",
"osVersion": "4.0"
},
"otnList": [
{
"type": "WIFI",
"modelId": "AVT-KR-TP1-24-AXXX00",
"versions": [
"12260401"
],
"visVersion": "260401"
},
{
"type": "Micom",
"modelId": "895010268241FFFFFFFF",
"versions": [
"25042102",
"FFFFFFFF"
],
"visVersion": "250421"
},
{
"type": "Micom",
"modelId": "09501026824110268341",
"versions": [
"25042102",
"25031801"
],
"visVersion": "250421"
}
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/connectionconfig/vs/0",
"rep": {
"autoReconnectionMinVersion": "1.0",
"autoReconnection": "true",
"autoReconnectionProtocolType": [
"helper_hotspot",
"ble_ocf"
],
"supportedWiFiAuthType": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"SAE"
],
"supportedWiFiCryptoType": [
"TKIP",
"AES",
"WEP-64",
"WEP-128"
],
"supportedWiFiFreq": [
"2.4G"
],
"calmConnectionCare": {
"version": "1.0",
"role": [
"things"
]
}
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**",
"connectedApSsid": "Branden_Summit"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
}
]
}
+476
View File
@@ -0,0 +1,476 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/personality/presence/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "",
"x.com.samsung.da.deviceId": "**REDACTED**",
"x.com.samsung.da.value": ""
}
]
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.longnotisubscription": "false",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/filter/airdustfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "54",
"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": "/temperature/control/vs/0",
"rep": {
"x.com.samsung.da.increment": "1"
}
},
{
"href": "/mode/convenient/vs/0",
"rep": {
"x.com.samsung.da.modes": "Off",
"x.com.samsung.da.supportedModes": [
"Off",
"Sleep",
"Quiet",
"Smart",
"Speed",
"Nano",
"NanoSleep"
]
}
},
{
"href": "/option/autoclean/vs/0",
"rep": {
"x.com.samsung.da.status": "Stop",
"x.com.samsung.da.settingStatus": "Off",
"x.com.samsung.da.progress": "0",
"x.com.samsung.da.supportedStatus": [
"Start",
"Stop"
],
"x.com.samsung.da.supportedSettingStatus": [
"On",
"Off"
]
}
},
{
"href": "/wind/strength/vs/0",
"rep": {
"x.com.samsung.da.modes": "0",
"x.com.samsung.da.supportedModes": [
"0",
"1",
"2",
"3",
"4"
],
"x.com.samsung.da.modesName": [
"Auto",
"Low",
"Mid",
"High",
"Turbo"
]
}
},
{
"href": "/wind/direction/vs/0",
"rep": {
"x.com.samsung.da.modes": "Left_And_Right",
"x.com.samsung.da.supportedModes": [
"Fix",
"Up_And_Low",
"Left_And_Right",
"All"
]
}
},
{
"href": "/alarms/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "ErrorCode_OFF",
"x.com.samsung.da.triggeredTime": "2026-07-27T06:07:25",
"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": "2026-07-27T06:07:25",
"x.com.samsung.da.state": "Deleted"
}
]
}
},
{
"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": "26.0",
"x.com.samsung.da.current": "30.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": "/temperature/current/0",
"rep": {
"range": [
16,
30
],
"units": "C",
"temperature": 30.0
}
},
{
"href": "/temperature/desired/0",
"rep": {
"range": [
16,
30
],
"units": "C",
"temperature": 26.0
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "0.000000",
"x.com.samsung.da.cumulativePower": "3193591",
"x.com.samsung.da.cumulativeSavedPower": "158331",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W"
}
},
{
"href": "/energy/consumption/0",
"rep": {
"power": 0.0
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"Auto",
"Cool",
"Dry",
"Wind"
],
"x.com.samsung.da.modes": [
"Dry"
],
"x.com.samsung.da.options": [
"Sleep_0",
"ArtificialWorking_Off",
"ComfortAICooling_Off",
"AiTempChanged_Off",
"AiTemp_270",
"OutdoorConnection_Connected",
"OutdoorTemp_88",
"CoolCapa_53",
"WarmCapa_0",
"Light_Off",
"Volume_100",
"StopAutoClean_Idle",
"Autoclean_Off",
"DiagnosisAI_Off",
"ProgressDiagnosisAI_1",
"ResultDiagnosisAI_Normal",
"SmartCoolClean_Off",
"ProgressSmartClean_0",
"FreezeAlarmSetting_On",
"DesiredFreezeAlarm_112",
"WashAlarm_Off",
"OptionCode_56440",
"ExtendOptionCode_246413",
"RacInfo_None",
"UpdateAllow_NotAllowed",
"DurationOn_0",
"WelcomeCoolingState_On"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off",
"x.com.samsung.da.displaycondition": "Enable"
}
},
{
"href": "/power/0",
"rep": {
"value": false
}
},
{
"href": "/sensors/vs/0",
"rep": {}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "ARA-WW-TP1-22-COMMON|10229641|60010535001511014600083200800000",
"x.com.samsung.da.description": "ARA-WW-TP1-22-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": "010",
"x.com.samsung.da.diagMinVersion": "1.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": "02378A260327",
"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": "102296A23012700",
"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": "102295A22113000,102299A10000300",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+08:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "0000000000",
"x.com.samsung.da.airconOptionList": [
"SingleCommand_1",
"DR",
"HOMECARE_WIZARD_V2",
"PRODUCT_GLOBAL",
"AI_RAC_GLOBAL_COOLONLY_3.0",
"AI_3.0",
"Auto_To_AI"
]
}
},
{
"href": "/humidity/0",
"rep": {
"humidity": 0
}
},
{
"href": "/humidity/vs/0",
"rep": {
"x.com.samsung.da.humidity": "0",
"x.com.samsung.da.fivepercentHumidity": "64"
}
},
{
"href": "/drlc/0",
"rep": {
"DRLevel": 1,
"start": "2026-07-27T05:07:32Z",
"duration": 20,
"override": false
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "1",
"x.com.samsung.da.duration": "20:47:00",
"x.com.samsung.da.drlcStartTime": "2026-07-27T05:07:32Z",
"x.com.samsung.da.override": "Off",
"x.com.samsung.da.realSaving": "Off"
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "000000B4012C016102490C000000",
"x.com.samsung.da.id": "RAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/keepnormalstate/vs/0",
"rep": {
"x.com.samsung.da.keepnormal": 1
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"x.com.samsung.da.newVersionNo": "00000000",
"x.com.samsung.da.currentVersionInfo": "00000000",
"otnStatus": "None",
"flashingProgress": "",
"otnTarget": "main",
"otnCompleteDate": "noHistory",
"otnList": [
{
"type": "WIFI",
"modelId": "ARA-WW-TP1-22-COMMON",
"versions": [
"11260327"
],
"visVersion": "260327"
},
{
"type": "Micom",
"modelId": "",
"versions": [
"23012700",
"FFFFFFFF"
],
"visVersion": "230127"
},
{
"type": "Micom",
"modelId": "",
"versions": [
"23012700",
"FFFFFFFF"
],
"visVersion": "230127"
}
]
}
},
{
"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": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Kuala_Lumpur",
"offset": "+08:00",
"DST": "OFF"
}
},
{
"href": "/option/muteonce/vs/0",
"rep": {
"muteonce": "Off"
}
},
{
"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": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**",
"connectedApSsid": "dreamer"
}
}
]
}
@@ -0,0 +1,515 @@
{
"meta": {
"model": "TP1X_DA-AC-RAC-01001_0000",
"device_type": "airconditioner",
"source": "user diagnostics (scrubbed)",
"note": "Cool-only global TP1X RAC variant (airconOptionList AI_RAC_GLOBAL_COOLONLY_3.0). Unlike the other TP1X_DA-AC-RAC dumps, its /otninformation/vs/0 ships no swVersionInfo block, so oneUiVersion is empty and detection must fall back to the hyphenated '-RAC-' modelNum token in for_device_by_model. Adds two hrefs absent from the other AC dumps -- /stepcontrol/vs/0 (manual airflow step) and /remotedeviceinfo/vs/0 (empty paired-device list), both ignored -- and exposes Samsung WindFree via the convenient-mode codes Nano/NanoSleep (in place of the Smart code on other boards; confirmed by an off-vs-on dump diff)."
},
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/personality/presence/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "",
"x.com.samsung.da.deviceId": "**REDACTED**",
"x.com.samsung.da.value": ""
}
]
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.longnotisubscription": "false",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/filter/airdustfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "100",
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterDesiredUsage": "500",
"x.com.samsung.da.filterStatus": "wash",
"x.com.samsung.da.filterCapacity": "500",
"x.com.samsung.da.filterCapacityUnit": "Hour",
"x.com.samsung.da.filterResetType": [
"replaceable",
"washable"
]
}
},
{
"href": "/temperature/control/vs/0",
"rep": {
"x.com.samsung.da.increment": "1"
}
},
{
"href": "/mode/convenient/vs/0",
"rep": {
"x.com.samsung.da.modes": "Off",
"x.com.samsung.da.supportedModes": [
"Off",
"Sleep",
"Quiet",
"Speed",
"Nano",
"NanoSleep"
]
}
},
{
"href": "/option/autoclean/vs/0",
"rep": {
"x.com.samsung.da.status": "Stop",
"x.com.samsung.da.settingStatus": "Off",
"x.com.samsung.da.progress": "0",
"x.com.samsung.da.supportedStatus": [
"Start",
"Stop"
],
"x.com.samsung.da.supportedSettingStatus": [
"On",
"Off"
]
}
},
{
"href": "/wind/strength/vs/0",
"rep": {
"x.com.samsung.da.modes": "0",
"x.com.samsung.da.supportedModes": [
"0",
"1",
"2",
"3",
"4"
],
"x.com.samsung.da.modesName": [
"Auto",
"Low",
"Mid",
"High",
"Turbo"
]
}
},
{
"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": "/alarms/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "ErrorCode_OFF",
"x.com.samsung.da.triggeredTime": "2026-07-26T15:24:44",
"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",
"x.com.samsung.da.triggeredTime": "2026-07-26T15:24:44",
"x.com.samsung.da.state": "Created"
}
]
}
},
{
"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": "20.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": "/temperature/current/0",
"rep": {
"range": [
16,
30
],
"units": "C",
"temperature": 20.0
}
},
{
"href": "/temperature/desired/0",
"rep": {
"range": [
16,
30
],
"units": "C",
"temperature": 25.0
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "0.000000",
"x.com.samsung.da.cumulativePower": "445938",
"x.com.samsung.da.cumulativeSavedPower": "81528",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W"
}
},
{
"href": "/energy/consumption/0",
"rep": {
"power": 0.0
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"Auto",
"Cool",
"Dry",
"Wind"
],
"x.com.samsung.da.modes": [
"Auto"
],
"x.com.samsung.da.options": [
"Sleep_0",
"ArtificialWorking_Off",
"ComfortAICooling_Off",
"AiTempChanged_Off",
"AiTemp_240",
"OutdoorTemp_84",
"CoolCapa_35",
"WarmCapa_0",
"Light_Off",
"Volume_Mute",
"StopAutoClean_Idle",
"Autoclean_Off",
"DiagnosisAI_Off",
"ProgressDiagnosisAI_0",
"ResultDiagnosisAI_Normal",
"OptionCode_52344",
"ExtendOptionCode_230029",
"RacInfo_None",
"UpdateAllow_NotAllowed",
"DurationOn_0",
"WelcomeCoolingState_Off"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off",
"causeSource": "DEFT",
"operationNumber": "0"
}
},
{
"href": "/power/0",
"rep": {
"value": false
}
},
{
"href": "/sensors/vs/0",
"rep": {}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_DA-AC-RAC-01001_0000|10247441|60010523001811014E00002200D00000",
"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": "AR2",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "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": "102474A23112200",
"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": "102702A24041900,102579A10000200",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "-03:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "0000000000",
"x.com.samsung.da.airconOptionList": [
"SingleCommand_1",
"DR",
"HOMECARE_WIZARD_V2",
"PRODUCT_GLOBAL",
"AI_RAC_GLOBAL_COOLONLY_3.0",
"AI_3.0",
"Auto_To_AI"
]
}
},
{
"href": "/humidity/0",
"rep": {
"humidity": 0
}
},
{
"href": "/humidity/vs/0",
"rep": {
"x.com.samsung.da.humidity": "0",
"x.com.samsung.da.fivepercentHumidity": "66"
}
},
{
"href": "/drlc/0",
"rep": {
"DRLevel": 1,
"start": "2026-07-26T12:01:41Z",
"duration": 23,
"override": false
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "1",
"x.com.samsung.da.duration": "23:59:00",
"x.com.samsung.da.drlcStartTime": "2026-07-26T12:01:41Z",
"x.com.samsung.da.override": "Off",
"x.com.samsung.da.realSaving": "Off"
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "000000A0012C0161024904000000",
"x.com.samsung.da.id": "RAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/stepcontrol/vs/0",
"rep": {
"x.com.samsung.da.modes": "Off",
"x.com.samsung.da.supportedModes": [
"Off",
"80",
"60",
"40",
"Power"
]
}
},
{
"href": "/keepnormalstate/vs/0",
"rep": {
"x.com.samsung.da.keepnormal": 1
}
},
{
"href": "/remotedatacontrol/vs/0",
"rep": {
"x.com.samsung.da.status": "Off",
"x.com.samsung.da.connectionStatus": "Disconnected"
}
},
{
"href": "/remotetemperature/vs/0",
"rep": {
"x.com.samsung.da.temperature": "",
"x.com.samsung.da.unit": "",
"x.com.samsung.da.error": ""
}
},
{
"href": "/remotedeviceinfo/vs/0",
"rep": {
"x.com.samsung.da.didList": ""
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"x.com.samsung.da.newVersionNo": "00000000",
"x.com.samsung.da.currentVersionInfo": "00000000",
"otnStatus": "None",
"flashingProgress": "",
"otnTarget": "main",
"otnCompleteDate": "noHistory",
"otnList": [
{
"type": "WIFI",
"modelId": "ARA-WW-TP1-24-ARXX00",
"versions": [
"11260327"
],
"visVersion": "260327"
},
{
"type": "Micom",
"modelId": "045210247441FFFFFFFF",
"versions": [
"23112200",
"FFFFFFFF"
],
"visVersion": "231122"
},
{
"type": "Micom",
"modelId": "04521027024110257941",
"versions": [
"24041900",
"10000200"
],
"visVersion": "240419"
}
]
}
},
{
"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": "/timezone/vs/0",
"rep": {
"timezoneid": "America/Sao_Paulo",
"offset": "-03:00",
"DST": "OFF"
}
},
{
"href": "/option/muteonce/vs/0",
"rep": {
"muteonce": "Off"
}
},
{
"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": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**",
"connectedApSsid": "**REDACTED**"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
}
]
}
+380
View File
@@ -0,0 +1,380 @@
{
"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": "2026-07-25T08:19:03"
},
{
"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": "2026-07-25T08:19:03"
}
]
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "0000000000",
"x.com.samsung.da.airconOptionList": [
"SingleCommand_1",
"HOMECARE_WIZARD_V2",
"PRODUCT_GLOBAL",
"AI_RAC_GLOBAL_2.0",
"AI_2.0",
"Auto_To_AI",
"AI_Heat"
]
}
},
{
"href": "/diagnosis/vs/0",
"rep": {
"x.com.samsung.da.diagnosisStart": "Ready"
}
},
{
"href": "/energy/consumption/0",
"rep": {
"power": 0.0
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "0",
"x.com.samsung.da.cumulativePower": "320466",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W"
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+00:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/filter/airdustfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "41",
"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": "ARTIK051_PRAC_20K|10217841|60010519001411010200001000000000",
"x.com.samsung.da.description": "ARTIK051_PRAC_20K",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"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": "02181A230313",
"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": "20082000,FFFFFFFF",
"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": "20091600,10000400"
}
]
}
},
{
"href": "/mode/convenient/vs/0",
"rep": {
"x.com.samsung.da.modes": "Off",
"x.com.samsung.da.supportedModes": [
"Off",
"Sleep",
"Quiet",
"Smart",
"Speed",
"MotionIndirect",
"MotionDirect",
"Nano",
"NanoSleep"
]
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"Cool",
"Dry",
"Wind",
"Auto",
"Heat"
],
"x.com.samsung.da.modes": [
"Dry"
],
"x.com.samsung.da.options": [
"Sleep_0",
"ArtificialWorking_Off",
"ComfortAICooling_Off",
"MotionState_Normal",
"AiTempChanged_Off",
"AiTemp_240",
"OutdoorTemp_87",
"CoolCapa_25",
"WarmCapa_32",
"Light_Off",
"Volume_100",
"MotionMode_Default",
"OptionCode_56376",
"ExtendOptionCode_781",
"RacInfo_None",
"UpdateAllow_NotAllowed",
"DurationOn_0",
"WelcomeCoolingState_Off"
]
}
},
{
"href": "/option/airpurify/vs/0",
"rep": {
"x.com.samsung.da.modes": "Off",
"x.com.samsung.da.supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/option/autoclean/vs/0",
"rep": {
"x.com.samsung.da.status": "Stop",
"x.com.samsung.da.settingStatus": "On",
"x.com.samsung.da.progress": "0",
"x.com.samsung.da.supportedStatus": [
"Start",
"Stop"
],
"x.com.samsung.da.supportedSettingStatus": [
"On",
"Off"
]
}
},
{
"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": "false",
"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 CleanLevel",
"x.com.samsung.da.type": "CleanLevel",
"x.com.samsung.da.value": [
"0"
]
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Sensor for Odor",
"x.com.samsung.da.type": "Odor",
"x.com.samsung.da.value": [
"0"
]
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Sensor for Dust",
"x.com.samsung.da.type": "Dust",
"x.com.samsung.da.value": [
"0",
"0"
]
},
{
"x.com.samsung.da.id": "3",
"x.com.samsung.da.description": "Sensor for FineDust",
"x.com.samsung.da.type": "FineDust",
"x.com.samsung.da.value": [
"0",
"0"
]
},
{
"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": [
"0",
"0"
]
}
]
}
},
{
"href": "/temperature/control/vs/0",
"rep": {
"x.com.samsung.da.increment": "1"
}
},
{
"href": "/temperature/current/0",
"rep": {
"range": [
16.0,
30.0
],
"units": "C",
"temperature": 27.0
}
},
{
"href": "/temperature/desired/0",
"rep": {
"range": [
16.0,
30.0
],
"units": "C",
"temperature": 26.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": "26.0",
"x.com.samsung.da.current": "27.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": {}
},
{
"href": "/wind/direction/vs/0",
"rep": {
"x.com.samsung.da.modes": "Fix",
"x.com.samsung.da.supportedModes": [
"Fix",
"All",
"Up_And_Low",
"Left_And_Right"
]
}
},
{
"href": "/wind/strength/vs/0",
"rep": {
"x.com.samsung.da.modes": "0",
"x.com.samsung.da.supportedModes": [
"0",
"1",
"2",
"3",
"4"
],
"x.com.samsung.da.modesName": [
"Auto",
"Low",
"Mid",
"High",
"Turbo"
]
}
}
]
}
@@ -0,0 +1,590 @@
{
"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.alarmType": "Device",
"x.com.samsung.da.code": "ErrorCode_OFF",
"x.com.samsung.da.triggeredTime": "2026-07-27T11:59:06",
"x.com.samsung.da.state": "Deleted"
}
],
"rt": [
"x.com.samsung.da.alarms"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/anomalyload/vs/0",
"rep": {
"operation": "Off",
"mode": "Alarm",
"supportedModes": [
"Alarm",
"PowerSaving"
],
"savingTime": "20",
"supportedSavingTime": [
"20",
"40",
"60"
]
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "000000A0012C01610CC90C0F0000",
"x.com.samsung.da.id": "RAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "0000000000",
"x.com.samsung.da.airconOptionList": [
"PRODUCT_GLOBAL",
"AI_GLOBAL_COOLONLY_4.0",
"AI_3.0",
"Auto_To_AI",
"ENERGY_2.0",
"HOMECARE_WIZARD_V2",
"DR",
"SingleCommand_1"
],
"InstalledRoomSize": "0.0",
"SkipInstalledRoomSize": "false",
"RoomSizeMenuNumber": "Manual"
}
},
{
"href": "/da/softreset/vs/0",
"rep": {
"x.com.samsung.da.softwarereset": "false"
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.start": "1970-01-01T00:00:00Z",
"x.com.samsung.da.override": "Off",
"x.com.samsung.da.durationminutes": "0",
"x.com.samsung.da.drlcLevel": "0",
"x.com.samsung.da.realSaving": "Off"
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativeDate": "1785153515",
"x.com.samsung.da.cumulativeSavedPower": "0",
"x.com.samsung.da.cumulativePower": "47536",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePowerType": "total"
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+08:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/filter/airdustfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterCapacity": "180",
"x.com.samsung.da.filterCapacityUnit": "Hour",
"x.com.samsung.da.filterResetType": [
"replaceable",
"washable"
],
"x.com.samsung.da.filterStatus": "normal",
"x.com.samsung.da.filterUsage": "44",
"x.com.samsung.da.filterDesiredUsage": "180",
"x.com.samsung.da.supportedFilterDesiredUsage": [
"180",
"300",
"500",
"700"
]
}
},
{
"href": "/humidity/vs/0",
"rep": {
"x.com.samsung.da.humidity": "0",
"x.com.samsung.da.fivepercentHumidity": "46"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.modelNum": "TP1X_DA-AC-RAC-01011_0000|10281241|6001051A001911018E0049300002A900",
"x.com.samsung.da.description": "TP1X_DA-AC-RAC-01011_0000",
"x.com.samsung.da.serialNum": "**REDACTED**",
"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": "02825A260401",
"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.newVersionAvailable": "0",
"x.com.samsung.da.number": "02812A25111800,FFFFFFFFFFFFFF"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.newVersionAvailable": "0",
"x.com.samsung.da.number": "02813A25101500,FFFFFFFFFFFFFF"
},
{
"x.com.samsung.da.id": "3",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.newVersionAvailable": "0",
"x.com.samsung.da.number": "FFFFFFFFFFFFFF,FFFFFFFFFFFFFF"
}
],
"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": "AR2",
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01"
}
},
{
"href": "/keepnormalstate/vs/0",
"rep": {
"x.com.samsung.da.keepnormal": 0
}
},
{
"href": "/light/vs/0",
"rep": {
"mode": "On",
"supportedModes": [
"On",
"Off"
]
}
},
{
"href": "/mds/absencemonitoring/vs/0",
"rep": {}
},
{
"href": "/mds/absencestate/vs/0",
"rep": {}
},
{
"href": "/mode/convenient/vs/0",
"rep": {
"x.com.samsung.da.modes": "Off",
"x.com.samsung.da.supportedModes": [
"Off",
"Sleep",
"Quiet",
"Smart",
"Speed",
"Nano",
"NanoSleep",
"DryComfort"
]
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.modes": [
"Cool"
],
"workingMode": "Cool",
"x.com.samsung.da.supportedModes": [
"Auto",
"Cool",
"Dry",
"Fan"
],
"x.com.samsung.da.options": [
"OptionCode_56416",
"Sleep_0",
"SleepRemainTime_0",
"ArtificialWorking_Off",
"ComfortAICooling_Off",
"AiTempChanged_Off",
"AiTemp_240",
"Volume_100",
"DiagnosisAI_Off",
"ProgressDiagnosisAI_0",
"ResultDiagnosisAI_Normal",
"SmartCoolClean_Off",
"ProgressSmartClean_0",
"OutDoorVentil_Off",
"InDoorVentil_Off",
"FreezeAlarmSetting_Off",
"DesiredFreezeAlarm_240",
"WashAlarm_Off",
"ExtendOptionCode_246413",
"UpdateAllow_NotAllowed",
"DurationOn_0",
"WelcomeCoolingState_Off"
],
"rt": [
"x.com.samsung.da.mode"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/option/autoclean/vs/0",
"rep": {
"x.com.samsung.da.status": "Stop",
"x.com.samsung.da.supportedStatus": [
"Start",
"Stop"
],
"x.com.samsung.da.progress": "0",
"x.com.samsung.da.settingStatus": "Off",
"x.com.samsung.da.supportedSettingStatus": [
"On",
"Off"
],
"defaultSettingStatus": "On"
}
},
{
"href": "/option/muteonce/vs/0",
"rep": {
"muteonce": "Off",
"rt": [
"x.com.samsung.da.option.muteonce"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/personality/presence/vs/0",
"rep": {}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off",
"operationNumber": "2",
"displayCondition": "Enable"
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.longnotisubscription": "false",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/reserverulesets/vs/0",
"rep": {
"x.com.samsung.da.sets": "8EFFFFFFFF101E121EFFFFFFFFFFFF000001001F0001000000000000009C00FFFF1E00",
"x.com.samsung.da.id": "RAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/selfcheck/vs/0",
"rep": {
"x.com.samsung.da.start": "Cancel",
"x.com.samsung.da.status": "Ready",
"x.com.samsung.da.progress": "0",
"x.com.samsung.da.result": "NA",
"x.com.samsung.da.supportedActions": [
"Start",
"Cancel"
],
"x.com.samsung.da.error": [
"DA_ERROR_NONE"
]
}
},
{
"href": "/temperature/control/vs/0",
"rep": {
"x.com.samsung.da.increment": "1"
}
},
{
"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.current": "29.0",
"x.com.samsung.da.desired": "18.0",
"x.com.samsung.da.minimum": "16",
"x.com.samsung.da.maximum": "30",
"x.com.samsung.da.increment": "1",
"x.com.samsung.da.unit": "Celsius"
}
]
}
},
{
"href": "/wind/oscillation/vs/0",
"rep": {
"vertical": "Fix",
"supportedVertical": [
"Swing",
"Fix"
],
"verticalAngle": "4",
"supportedVerticalAngle": [
"1",
"2",
"3",
"4",
"5",
"6"
],
"horizontal": "Fix",
"supportedHorizontal": [
"Swing",
"Fix"
],
"horizontalSwingMode": "AllSwing",
"supportedHorizontalSwingMode": [
"AllSwing",
"LeftSwing",
"RightSwing"
],
"horizontalAngle": "3",
"supportedHorizontalAngle": [
"1",
"2",
"3",
"4",
"5"
]
}
},
{
"href": "/wind/strength/vs/0",
"rep": {
"x.com.samsung.da.modes": "1",
"x.com.samsung.da.supportedModes": [
"0",
"1",
"2",
"3",
"4"
],
"x.com.samsung.da.modesName": [
"Auto",
"Low",
"Mid",
"High",
"Turbo"
]
}
},
{
"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 Air conditioner",
"osVersion": "4.0"
},
"otnList": [
{
"type": "WIFI",
"modelId": "ARA-WW-TP1-26-AR6X00MG",
"versions": [
"11260401"
],
"visVersion": "260401"
},
{
"type": "Micom",
"modelId": "845241131741FFFFFFFF",
"versions": [
"25102900",
"FFFFFFFF"
],
"visVersion": "251029"
},
{
"type": "Micom",
"modelId": "045210281241FFFFFFFF",
"versions": [
"25111800",
"FFFFFFFF"
],
"visVersion": "251118"
},
{
"type": "Micom",
"modelId": "045210281341FFFFFFFF",
"versions": [
"25101500",
"FFFFFFFF"
],
"visVersion": "251015"
},
{
"type": "Micom",
"modelId": "0452FFFFFFFFFFFFFFFF",
"versions": [
"FFFFFFFF",
"FFFFFFFF"
],
"visVersion": "999999"
}
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Kuala_Lumpur",
"offset": "+08:00",
"DST": "OFF"
}
},
{
"href": "/connectionconfig/vs/0",
"rep": {
"autoReconnectionMinVersion": "1.0",
"autoReconnection": "true",
"autoReconnectionProtocolType": [
"helper_hotspot",
"ble_ocf"
],
"supportedWiFiAuthType": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"SAE"
],
"supportedWiFiCryptoType": [
"TKIP",
"AES",
"WEP-64",
"WEP-128"
],
"supportedWiFiFreq": [
"2.4G"
],
"calmConnectionCare": {
"version": "1.0",
"role": [
"things"
]
}
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**",
"connectedApSsid": "K9 Unit"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
},
{
"href": "/dginformation/vs/0",
"rep": {
"enrolmentstatus": "Unknown",
"devicestate": "Unknown",
"lockstatus": "Unknown",
"nextduedate": "",
"workingminutes": 0,
"paymentinfo": {
"emiplan": "Unknown",
"currency": "Unknown",
"totalemi": 0,
"totalemipaid": 0
}
}
},
{
"href": "/temperature/desired/0",
"rep": {
"temperature": 25
}
},
{
"href": "/power/0",
"rep": {
"value": false
}
}
]
}
+523
View File
@@ -0,0 +1,523 @@
{
"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": "2026-07-26T06:26:40",
"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": "2026-07-26T06:26:40",
"x.com.samsung.da.state": "Deleted"
}
]
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "8D0D00B4012C3F21064804000000",
"x.com.samsung.da.id": "RAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "2647000000",
"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": 244.0
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "244.000000",
"x.com.samsung.da.cumulativePower": "11001",
"x.com.samsung.da.cumulativeSavedPower": "0",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePowerType": "total"
}
},
{
"href": "/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": "20",
"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": "60"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_DA_AC_WAC_01001_0000|10254341|60050000001711014E00402000002000",
"x.com.samsung.da.description": "TP1X_DA_AC_WAC_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": "AW0",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02545A260601",
"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": "02543A24061800,FFFFFFFFFFFFFF",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Outdoor",
"x.com.samsung.da.number": "02580A10000100,FFFFFFFFFFFFFF"
},
{
"x.com.samsung.da.id": "3",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Touch IC",
"x.com.samsung.da.number": "02553A23031600,FFFFFFFFFFFFFF"
}
]
}
},
{
"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",
"Quiet",
"Nano",
"NanoSleep"
]
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"AIComfort",
"Cool",
"Dry",
"Fan"
],
"x.com.samsung.da.modes": [
"Cool"
],
"x.com.samsung.da.options": [
"OnTimer_0",
"OffTimer_0",
"ArtificialWorking_Off",
"ComfortAICooling_Off",
"AiTempChanged_Off",
"AiTemp_230",
"Volume_Mute",
"OutdoorTemp_97",
"CoolCapa_0",
"StopAutoClean_Idle",
"Autoclean_Off",
"OptionCode_51320",
"ExtendOptionCode_230093",
"RacInfo_None",
"UpdateAllow_NotAllowed",
"DurationOn_93",
"WelcomeCoolingState_Off"
]
}
},
{
"href": "/option/autoclean/vs/0",
"rep": {
"x.com.samsung.da.status": "Stop",
"x.com.samsung.da.settingStatus": "Off",
"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": "AWA-KR-TP1-23-AW7000",
"versions": [
"15260601"
],
"visVersion": "260601"
},
{
"type": "Micom",
"modelId": "042010254341FFFFFFFF",
"versions": [
"24061800",
"FFFFFFFF"
],
"visVersion": "240618"
},
{
"type": "Micom",
"modelId": "042010258041FFFFFFFF",
"versions": [
"10000100",
"FFFFFFFF"
],
"visVersion": "100001"
},
{
"type": "Micom",
"modelId": "042010255341FFFFFFFF",
"versions": [
"23031600",
"FFFFFFFF"
],
"visVersion": "230316"
}
]
}
},
{
"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": "/reserverulesets/vs/0",
"rep": {
"x.com.samsung.da.sets": "8DFFFFFFFF121E121E0000FFFFFFFFFFFF01000000011F0000000000000000001F8C00",
"x.com.samsung.da.id": "WAC",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/selfcheck/vs/0",
"rep": {
"x.com.samsung.da.start": "Cancel",
"x.com.samsung.da.status": "Ready",
"x.com.samsung.da.progress": "0",
"x.com.samsung.da.result": "NA",
"x.com.samsung.da.supportedActions": [
"Start",
"Cancel"
],
"x.com.samsung.da.error": [
"DA_ERROR_NONE"
]
}
},
{
"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": 30.0
}
},
{
"href": "/temperature/desired/0",
"rep": {
"range": [
16,
30
],
"units": "C",
"temperature": 30.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": "30.0",
"x.com.samsung.da.current": "30.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": "/welcome/temperature/vs/0",
"rep": {
"operatingStatus": "None",
"requestId": "0000"
}
},
{
"href": "/wind/direction/vs/0",
"rep": {
"x.com.samsung.da.modes": "Fix",
"x.com.samsung.da.supportedModes": [
"Fix",
"Left_And_Right"
]
}
},
{
"href": "/wind/strength/vs/0",
"rep": {
"x.com.samsung.da.modes": "1",
"x.com.samsung.da.supportedModes": [
"0",
"1",
"2",
"3",
"4",
"5"
],
"x.com.samsung.da.modesName": [
"Auto",
"1",
"2",
"3",
"4",
"5"
]
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**",
"connectedApSsid": "**REDACTED**"
}
}
]
}
+386
View File
@@ -0,0 +1,386 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/personality/presence/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "",
"x.com.samsung.da.deviceId": "**REDACTED**",
"x.com.samsung.da.value": ""
}
]
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.longnotisubscription": "false",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/filter/airdustfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "100",
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterDesiredUsage": "224",
"x.com.samsung.da.filterStatus": "wash",
"x.com.samsung.da.filterCapacity": "224",
"x.com.samsung.da.filterCapacityUnit": "Hour",
"x.com.samsung.da.filterResetType": [
"washable"
],
"x.com.samsung.da.supportedFilterDesiredUsage": [
"224",
"300",
"500",
"700"
]
}
},
{
"href": "/mode/convenient/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"Off",
"Sleep"
]
}
},
{
"href": "/option/autoclean/vs/0",
"rep": {
"x.com.samsung.da.status": "Stop",
"x.com.samsung.da.settingStatus": "On",
"x.com.samsung.da.progress": "100",
"cleaningTime": "30",
"minCleaningTime": "10",
"maxCleaningTime": "60",
"cleaningTimeUnit": "10",
"x.com.samsung.da.supportedStatus": [
"Start",
"TimedClean",
"Stop"
],
"x.com.samsung.da.supportedSettingStatus": [
"On",
"TimedClean",
"Off"
]
}
},
{
"href": "/alarms/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "ErrorCode_OFF",
"x.com.samsung.da.triggeredTime": "2026-07-26T10:35:53",
"x.com.samsung.da.state": "Deleted"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Alarm",
"x.com.samsung.da.alarmType": "Device",
"x.com.samsung.da.code": "WaterTankFull_OFF",
"x.com.samsung.da.triggeredTime": "2026-07-26T10:35:53",
"x.com.samsung.da.state": "Deleted"
}
]
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "259.000000",
"x.com.samsung.da.cumulativePower": "183099",
"x.com.samsung.da.cumulativeSavedPower": "0",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePowerType": "total"
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"Smart",
"Max",
"High",
"Medium",
"Quiet",
"ClothesDrying"
],
"x.com.samsung.da.modes": [
"Smart"
],
"x.com.samsung.da.options": [
"Volume_Mute",
"RacInfo_None",
"UpdateAllow_NotAllowed",
"WelcomeCoolingState_Off"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "On",
"causeSource": "SMTS"
}
},
{
"href": "/sensors/vs/0",
"rep": {}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_DA_AC_DHM_01001_0000|10253841|77000000001700000A00000000000000",
"x.com.samsung.da.description": "TP1X_DA_AC_DHM_01001_0000",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
"x.com.samsung.da.diagLogType": [
"errCode",
"dump"
],
"x.com.samsung.da.diagDumpType": "file",
"x.com.samsung.da.diagEndPoint": "SSM",
"x.com.samsung.da.diagMnid": "0AJT",
"x.com.samsung.da.diagSetupid": "AY0",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02544A260401",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "02538A24032700,FFFFFFFFFFFFFF",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Outdoor",
"x.com.samsung.da.number": "02590A10000100,FFFFFFFFFFFFFF"
},
{
"x.com.samsung.da.id": "4",
"x.com.samsung.da.description": "Version",
"x.com.samsung.da.type": "Touch IC",
"x.com.samsung.da.number": "02552A23020200,FFFFFFFFFFFFFF"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+09:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "0000000000",
"x.com.samsung.da.airconOptionList": [
"SingleCommand_1",
"DR",
"HOMECARE_WIZARD_V2",
"PRODUCT_GLOBAL",
"AI_RAC_GLOBAL_2.0",
"AI_Heat"
]
}
},
{
"href": "/humidity/vs/0",
"rep": {
"x.com.samsung.da.humidity": "47",
"x.com.samsung.da.desiredHumidity": "50",
"increment": "5",
"x.com.samsung.da.fivepercentHumidity": "46"
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "0",
"x.com.samsung.da.durationminutes": "0",
"x.com.samsung.da.start": "1970-01-01T00:00:00Z",
"x.com.samsung.da.override": "Off",
"x.com.samsung.da.realSaving": "Off"
}
},
{
"href": "/availablecontrolsets/vs/0",
"rep": {
"x.com.samsung.da.sets": "20001E46070A5A00000000000000",
"x.com.samsung.da.id": "DHM",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/da/softreset/vs/0",
"rep": {
"x.com.samsung.da.softwarereset": "false"
}
},
{
"href": "/keepnormalstate/vs/0",
"rep": {
"x.com.samsung.da.keepnormal": 0
}
},
{
"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": "ADH-WW-TP1-23-AY7100",
"versions": [
"18260401"
],
"visVersion": "260401"
},
{
"type": "Micom",
"modelId": "042110253841FFFFFFFF",
"versions": [
"24032700",
"FFFFFFFF"
],
"visVersion": "240327"
},
{
"type": "Micom",
"modelId": "042110259041FFFFFFFF",
"versions": [
"10000100",
"FFFFFFFF"
],
"visVersion": "100001"
},
{
"type": "Micom",
"modelId": "042110255241FFFFFFFF",
"versions": [
"23020200",
"FFFFFFFF"
],
"visVersion": "230202"
}
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/option/muteonce/vs/0",
"rep": {
"muteonce": "Off"
}
},
{
"href": "/reserverulesets/vs/0",
"rep": {
"x.com.samsung.da.sets": "3F1E461E461E46FFFFFFFF1E4600",
"x.com.samsung.da.id": "DHM",
"x.com.samsung.da.version": "1.0"
}
},
{
"href": "/connectionconfig/vs/0",
"rep": {
"autoReconnectionMinVersion": "1.0",
"autoReconnection": "true",
"autoReconnectionProtocolType": [
"helper_hotspot",
"ble_ocf"
],
"supportedWiFiAuthType": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"SAE"
],
"supportedWiFiCryptoType": [
"TKIP",
"AES",
"WEP-64",
"WEP-128"
],
"supportedWiFiFreq": [
"2.4G"
],
"calmConnectionCare": {
"version": "1.0",
"role": [
"things"
]
}
}
},
{
"href": "/welcome/humidity/vs/0",
"rep": {
"requestId": "0000",
"operatingStatus": "None"
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
}
]
}
+391
View File
@@ -0,0 +1,391 @@
{
"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": "US"
}
},
{
"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": "/course/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2"
],
"x.com.samsung.da.options": [
"DeviceType_0152",
"UpdateAllow_NotAllowed",
"Course_01",
"AiOption_On",
"MixedLoadBell_Disable",
"MixedLoadBellNoti_Nothing",
"LaundryOutTime_0",
"SeamlessControl_Enable",
"KidsLockBypass_On",
"DetergentOnce_1",
"DetergentLeft_0",
"DetergentBase_5",
"DetergentAlarm_Off",
"DetergentType_3",
"DetergentTotal_0",
"SpecialFunction_20",
"AvailableDelayTime_39",
"LaundryPlannerUserSetTime_0",
"ProgressTimeSet_B20870B400B4",
"WrinklePreventRunning_Off",
"DryTime_0",
"SendToDevice_Off",
"GMT_F6",
"DryTimeSet_0A00000000500A005A00000000000000001E0A005A00000000000000001414005A000000000000000000000000000000000000000000000000000000000000000000000000140A005A280A005A",
"EnergyLevelSet_0504040504040205020204040305020203030103",
"MostUsed_068A3ED000",
"WrinklePreventSet_0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F",
"MixedLoadBellSet_02FF02FFFF02FFFFFF0202FF02FFFF02FFFFFF",
"GeoFenceAlarm",
"UsagesDB_ok",
"EnergyKW_396",
"DrumCleanLog_Empty",
"TimeSync_NotSupported"
],
"x.com.samsung.da.supportedOptions": [
"2018410D33E06833ED0002F8520D33E178520D3083E8520D000078102D33E338520D520358520D000348520D0000E8520D33E058520D33E308410D308318410D33E098410D3080F8204D308328308D33E2E8102D308368000D0000D8102D000"
]
}
},
{
"href": "/cycleinterface/vs/0",
"rep": {
"x.com.samsung.da.cycleInterfaceEnabled": "On"
}
},
{
"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": "2323800",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.cumulativeDate": "1784991600",
"x.com.samsung.da.cumulativeDateUTC": "1785009600"
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "-05:00"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "DA_WM_TP1_21_COMMON|20286441|300000010015110002A3031700000000",
"x.com.samsung.da.description": "DA_WM_TP1_21_COMMON_DVE50A8800_8600/DC92-02835A_0080",
"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": "310",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "DA_WM_TP1_21_COMMON|20286441|300000010015110002A3031700000000",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02986A260118(A182)",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Firmware_1_DB_20286441210514150FFFFF202835412105131904FFFF(01522028644120283541_30000000)(FileDown:0)(Type:0)",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "02864A21051415,02835A21051319",
"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",
"Drying",
"Cooling",
"Finish"
],
"currentJobState": "None",
"remainingTime": "01:15:00",
"progressPercentage": "1"
}
},
{
"href": "/operational/state/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.remainingTime": "01:15:00",
"x.com.samsung.da.progressPercentage": "1",
"x.com.samsung.da.progress": "None",
"x.com.samsung.da.delayEndTime": "00:00:00",
"x.com.samsung.da.supportedProgress": [
"None",
"Drying",
"Cooling",
"Finish"
]
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"x.com.samsung.da.newVersionNo": "00000000",
"x.com.samsung.da.currentVersionInfo": "00000000",
"otnStatus": "None",
"flashingProgress": "",
"otnTarget": "main",
"otnCompleteDate": "2026-03-04",
"otnList": [
{
"type": "WIFI",
"modelId": "DA_WM_TP1_21_COMMON",
"versions": [
"30260118"
],
"visVersion": "260118"
},
{
"type": "Micom",
"modelId": "01522028644120283541",
"versions": [
"21051415",
"21051319"
],
"visVersion": "210514"
}
]
}
},
{
"href": "/power/0",
"rep": {
"value": false
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
},
{
"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": "/setting/vs/0",
"rep": {
"x.com.samsung.da.supportedSetLanguage": [
"en_US",
"ko_KR",
"fr_CA",
"es_MX",
"pt_BR"
],
"x.com.samsung.da.setLanguage": "en_US"
}
},
{
"href": "/st/dryercourse/vs/0",
"rep": {
"x.com.samsung.da.st.dryerMode": "Table_03_Course_01",
"x.com.samsung.da.st.courseTable": "Table_03"
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "America/Chicago",
"offset": "-05:00",
"DST": "ON"
}
},
{
"href": "/washer/vs/0",
"rep": {
"x.com.samsung.da.wrinklePrevent": "Off",
"x.com.samsung.da.waterTemperature": "Medium",
"x.com.samsung.da.supportedWaterTemperature": [
"None",
"ExtraLow",
"Low",
"MediumLow",
"Medium",
"High"
],
"x.com.samsung.da.dryLevel": "Normal",
"x.com.samsung.da.supportedDryLevel": [
"None",
"Damp",
"Less",
"Normal",
"More",
"Very"
],
"x.com.samsung.da.dryerType": "Electricity"
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**"
}
},
{
"href": "/wm/editcourse/vs/0",
"rep": {
"x.com.samsung.da.editCourseList": "EditCourseList_0106",
"x.com.samsung.da.fixedCourseList": "FixedCourseList_0101"
}
},
{
"href": "/wm/jobbeginingstatus/vs/0",
"rep": {
"x.com.samsung.da.currentStatus": "None"
}
},
{
"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": "true",
"x.com.samsung.da.aiCourse": "false",
"x.com.samsung.da.isModelSettingPowerOnOff": "false",
"x.com.samsung.da.modelCode": "M(None),W(DVE50A8600V/A3)"
}
},
{
"href": "/wm/welcomemsg/vs/0",
"rep": {}
}
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"state_keys": [
"alarm_code",
"child_lock",
"clean_level",
"device_active",
"display",
"display_light",
"dust",
"energy_kwh",
"energy_saved_kwh",
"fan",
"fine_dust",
"firmware_update",
"hepa_filter_status",
"hepa_filter_usage",
"mute_once",
"odor",
"panel_status",
"pet_filter_activation",
"power_switch",
"sound_mode",
"sound_output",
"sound_volume",
"super_fine_dust"
]
}
+3
View File
@@ -6,9 +6,12 @@
"alarm_code",
"auto_clean",
"climate",
"current_temperature_c",
"diagnosis_status",
"display_light",
"energy_kwh",
"energy_saved_kwh",
"humidity",
"power_watts"
]
}
+17
View File
@@ -0,0 +1,17 @@
{
"state_keys": [
"air_filter_status",
"air_filter_usage",
"alarm_code",
"auto_clean",
"climate",
"current_temperature_c",
"display_light",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"humidity",
"mute_once",
"power_watts"
]
}
+3
View File
@@ -5,10 +5,13 @@
"alarm_code",
"auto_clean",
"climate",
"current_temperature_c",
"diagnosis_status",
"display_light",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"humidity",
"mute_once",
"power_watts"
]
@@ -6,10 +6,12 @@
"alarm_code",
"auto_clean",
"climate",
"current_temperature_c",
"display_light",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"humidity",
"mute_once",
"selfcheck_error",
"selfcheck_result",
+1
View File
@@ -8,6 +8,7 @@
"climate",
"current_limit_enabled",
"current_limit_level",
"current_temperature_c",
"display_light",
"energy_kwh",
"energy_saved_kwh",
@@ -0,0 +1,17 @@
{
"state_keys": [
"air_filter_status",
"air_filter_usage",
"alarm_code",
"auto_clean",
"climate",
"current_temperature_c",
"display_light",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"humidity",
"mute_once",
"power_watts"
]
}
@@ -5,8 +5,11 @@
"alarm_code",
"auto_clean",
"climate",
"current_temperature_c",
"display_light",
"energy_kwh",
"firmware_update",
"humidity",
"mute_once",
"power_watts"
]
+16
View File
@@ -0,0 +1,16 @@
{
"state_keys": [
"air_filter_status",
"air_filter_usage",
"air_purify",
"alarm_code",
"auto_clean",
"climate",
"current_temperature_c",
"diagnosis_status",
"display_light",
"energy_kwh",
"humidity",
"power_watts"
]
}
@@ -0,0 +1,21 @@
{
"state_keys": [
"air_filter_status",
"air_filter_usage",
"alarm_code",
"auto_clean",
"climate",
"current_temperature_c",
"display_light",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"humidity",
"mute_once",
"overload_protection_active",
"overload_protection_mode",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status"
]
}
+20
View File
@@ -0,0 +1,20 @@
{
"state_keys": [
"air_filter_status",
"air_filter_usage",
"alarm_code",
"auto_clean",
"climate",
"current_temperature_c",
"display_light",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"humidity",
"mute_once",
"power_watts",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status"
]
}
+17
View File
@@ -0,0 +1,17 @@
{
"state_keys": [
"air_filter_status",
"air_filter_usage",
"alarm_code",
"auto_clean",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"humidity",
"mute_once",
"operating_mode",
"power_switch",
"power_watts",
"target_humidity"
]
}
+1
View File
@@ -3,6 +3,7 @@
"alarm_code",
"auto_release_dry",
"child_lock",
"completion_minutes",
"cycle",
"cycle_active",
"delay_start_hours",
+1
View File
@@ -3,6 +3,7 @@
"alarm_code",
"buzzer_sound",
"child_lock",
"completion_minutes",
"cycle",
"cycle_active",
"delay_start_hours",
+24
View File
@@ -0,0 +1,24 @@
{
"state_keys": [
"alarm_code",
"child_lock",
"completion_minutes",
"cycle",
"cycle_active",
"delay_start_hours",
"diagnosis",
"dry_level",
"dry_time",
"dryer_type",
"energy_kwh",
"finish_time",
"firmware_update",
"job_beginning_status",
"machine_state",
"power_switch",
"progress",
"progress_percentage",
"remote_control",
"wrinkle_prevent"
]
}
+32
View File
@@ -0,0 +1,32 @@
{
"state_keys": [
"alarm_code",
"burner_0_hot_surface",
"burner_0_pan_detected",
"burner_0_power_level",
"burner_0_state",
"burner_1_hot_surface",
"burner_1_pan_detected",
"burner_1_power_level",
"burner_1_state",
"burner_2_hot_surface",
"burner_2_pan_detected",
"burner_2_power_level",
"burner_2_state",
"cooktop_child_lock",
"cooktop_power",
"cooktop_state",
"energy_kwh",
"firmware_update",
"paired_hood_connected",
"paired_hood_fan_speed",
"paired_hood_firmware",
"paired_hood_light",
"paired_hood_model",
"paired_hood_power",
"probe_battery",
"probe_connected",
"probe_target_temperature",
"probe_temperature"
]
}
+25
View File
@@ -0,0 +1,25 @@
{
"state_keys": [
"alarm_code",
"child_lock",
"cloud_connected",
"cook_time",
"current_temp_c",
"cycle_active",
"door_open",
"energy_kwh",
"fast_preheat",
"finish_time",
"firmware_update",
"lamp",
"machine_state",
"natural_steam",
"operation_time_minutes",
"oven_mode",
"oven_setpoint",
"oven_state",
"progress_percentage",
"remote_control",
"sound"
]
}
+6
View File
@@ -2,20 +2,26 @@
"state_keys": [
"alarm_code",
"burner_0_hot_surface",
"burner_0_pan_detected",
"burner_0_power_level",
"burner_0_state",
"burner_1_hot_surface",
"burner_1_pan_detected",
"burner_1_power_level",
"burner_1_state",
"burner_2_hot_surface",
"burner_2_pan_detected",
"burner_2_power_level",
"burner_2_state",
"burner_3_hot_surface",
"burner_3_pan_detected",
"burner_3_power_level",
"burner_3_state",
"child_lock",
"cloud_connected",
"cook_time",
"cooktop_child_lock",
"cooktop_power",
"cooktop_safety_shutoff_enabled",
"cooktop_state",
"current_temp_c",
+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"
]
}
+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"
]
}
@@ -0,0 +1,22 @@
{
"state_keys": [
"alarm_code",
"defrost_active",
"defrost_delay",
"diagnosis_status",
"door_onedoorfreezer_open",
"energy_kwh",
"energy_last_month_kwh",
"energy_saved_kwh",
"energy_this_month_kwh",
"firmware_update",
"freezer_setpoint",
"freezer_temperature",
"ice_maker_enabled",
"power_energy_kwh",
"power_watts",
"rapid_freezing",
"rapid_fridge",
"sabbath_mode"
]
}
@@ -0,0 +1,23 @@
{
"state_keys": [
"alarm_code",
"cooler_setpoint",
"cooler_temperature",
"defrost_active",
"defrost_delay",
"diagnosis_status",
"door_cooler_open",
"door_onedoorfreezer_open",
"energy_kwh",
"energy_last_month_kwh",
"energy_saved_kwh",
"energy_this_month_kwh",
"firmware_update",
"ice_maker_enabled",
"power_energy_kwh",
"power_watts",
"rapid_freezing",
"rapid_fridge",
"sabbath_mode"
]
}
+19
View File
@@ -0,0 +1,19 @@
{
"state_keys": [
"alarm_code",
"auto_empty",
"cleanstation_status",
"discharging_time",
"dustbag_full",
"dustbag_usage",
"dustbin_auto_close",
"energy_kwh",
"firmware_update",
"stick_status",
"uvc_emitted_time",
"uvc_finished_time",
"uvc_intensive_mode",
"uvc_operation_time",
"uvc_total_operation_time"
]
}
+1
View File
@@ -5,6 +5,7 @@
"bubble_soak",
"buzzer_sound",
"child_lock",
"completion_minutes",
"cycle",
"cycle_active",
"delay_start_hours",
+1
View File
@@ -3,6 +3,7 @@
"alarm_code",
"bubble_soak",
"child_lock",
"completion_minutes",
"cycle",
"cycle_active",
"delay_start_hours",
+1
View File
@@ -2,6 +2,7 @@
"state_keys": [
"alarm_code",
"child_lock",
"completion_minutes",
"cycle",
"cycle_active",
"delay_start_hours",
+30
View File
@@ -0,0 +1,30 @@
{
"state_keys": [
"alarm_code",
"child_lock",
"completion_minutes",
"cycle",
"cycle_active",
"delay_start_hours",
"detergent_low",
"detergent_quantity",
"detergent_water_hardness",
"diagnosis_status",
"drum_clean_cycles_remaining",
"energy_kwh",
"finish_time",
"firmware_update",
"job_beginning_status",
"machine_state",
"power_switch",
"progress",
"progress_percentage",
"remote_control",
"rinse_cycles",
"softener_concentration",
"softener_low",
"softener_quantity",
"spin_speed",
"wash_temperature"
]
}
+26
View File
@@ -0,0 +1,26 @@
{
"state_keys": [
"alarm_code",
"child_lock",
"completion_minutes",
"cycle",
"cycle_active",
"delay_start_hours",
"detergent_low",
"diagnosis_status",
"drum_clean_cycles_remaining",
"energy_kwh",
"finish_time",
"firmware_update",
"job_beginning_status",
"machine_state",
"power_switch",
"progress",
"progress_percentage",
"remote_control",
"rinse_cycles",
"softener_low",
"spin_speed",
"wash_temperature"
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"state_keys": [
"alarm_code",
"buzz_lock",
"coldwater_lock",
"dispense_capacity",
"dispense_type",
"favorite_capacity",
"favorite_capacity_enabled",
"filter_clean_remain_time",
"filter_door_status",
"filter_status",
"filter_usage",
"firmware_update",
"hot_water_temperature",
"hotwater_lock",
"pouring",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status",
"sterilize_last_time",
"sterilize_period",
"sterilize_plan_time",
"sterilize_run_time",
"waterpurifier_status"
]
}
+31
View File
@@ -0,0 +1,31 @@
{
"state_keys": [
"alarm_code",
"buzz_lock",
"coffee_brew_status",
"coldwater_lock",
"dispense_capacity",
"dispense_type",
"favorite_capacity",
"favorite_capacity_enabled",
"favorite_coffee_enabled",
"favorite_hotwater_enabled",
"favorite_hotwater_temperature",
"filter_clean_remain_time",
"filter_door_status",
"filter_status",
"filter_usage",
"firmware_update",
"hot_water_temperature",
"hotwater_lock",
"pouring",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status",
"sterilize_last_time",
"sterilize_period",
"sterilize_plan_time",
"sterilize_run_time",
"waterpurifier_status"
]
}
+243
View File
@@ -0,0 +1,243 @@
{
"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": "CT_E_OFF",
"x.com.samsung.da.triggeredTime": "2026-07-26T06:30:18"
}
]
}
},
{
"href": "/bluetooth/hood/status/vs/0",
"rep": {
"connectionState": "disconnected",
"micomModelId": "",
"firmwareVersion": "",
"power": "off",
"fanSpeed": 0,
"lampState": "off",
"timer": {}
}
},
{
"href": "/bluetooth/probe/status/vs/0",
"rep": {
"connectionState": "disconnected",
"batteryState": "normal",
"batteryPercentage": 0,
"operationBurnerNumber": -1,
"temperatureUnit": "C",
"targetTemperature": 0,
"currentTemperature": 0
}
},
{
"href": "/cooktop/recipe/status/vs/0",
"rep": {
"sequenceNumber": 160,
"operationBurnerNumber": 2,
"stageInfo": {
"numberOfStages": 0,
"cookingTimeList": []
},
"textData": {
"menu": ""
}
}
},
{
"href": "/cooktop/spec/vs/0",
"rep": {
"deviceType": "cooktop",
"micomModelId": "NV8500T-/KO4",
"supportedFeatureList": [
"kitchenService"
],
"numberOfBurners": 3,
"supportedBurnerList": [
0,
1,
2
],
"supportedPowerLevelList": [
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"boost",
"0",
"keepWarm"
],
"supportedRecipeProtocolVersion": 3
}
},
{
"href": "/cooktop/status/vs/0",
"rep": {
"sequenceNumber": 3214,
"power": "off",
"childLock": "off",
"operationState": "ready",
"additionalStateList": [
""
],
"burnerList": [
{
"burnerNumber": 0,
"mode": "normal",
"operationState": "ready",
"powerLevel": "0",
"hotSurfaceState": "normal",
"panDetection": true,
"timer": {
"operationState": "ready",
"cookingTime": 0,
"remainingTime": 0
}
},
{
"burnerNumber": 1,
"mode": "normal",
"operationState": "ready",
"powerLevel": "0",
"hotSurfaceState": "normal",
"panDetection": true,
"timer": {
"operationState": "ready",
"cookingTime": 0,
"remainingTime": 0
}
},
{
"burnerNumber": 2,
"mode": "normal",
"operationState": "ready",
"powerLevel": "0",
"hotSurfaceState": "normal",
"panDetection": true,
"timer": {
"operationState": "ready",
"cookingTime": 0,
"remainingTime": 0
}
}
]
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "-500",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePower": "212700",
"x.com.samsung.da.cumulativeUnit": "Wh"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_DA-KS-COOKTOP-01011|40459741|50000203001711000A00000000000000",
"x.com.samsung.da.description": "NV8500T-/KO4",
"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": "26033000",
"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": "04597A23021100, 04596A23080300",
"x.com.samsung.da.newVersionAvailable": "0"
}
],
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
"x.com.samsung.da.diagMinVersion": "3.0",
"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": "KCA",
"x.com.samsung.da.diagTsId": "DA01"
}
},
{
"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",
"flashingProgress": "",
"otnTarget": "main",
"otnStatus": "None",
"otnCompleteDate": "noHistory",
"otnList": [
{
"type": "WIFI",
"modelId": "AKS-WW-TP1-21-COOKTOP-2",
"versions": [
"40260330"
],
"visVersion": "260330"
},
{
"type": "Micom",
"modelId": "05114045974140459641",
"versions": [
"23021100",
"23080300"
],
"visVersion": "230803"
}
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,201 @@
{
"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": "/bespoke/vs/0",
"rep": {
"x.com.samsung.da.BespokeProduct": "Off"
}
},
{
"href": "/defrost/delay/0",
"rep": {}
},
{
"href": "/defrost/delay/vs/0",
"rep": {}
},
{
"href": "/diagnosis/vs/0",
"rep": {
"x.com.samsung.da.diagnosisStart": "Ready"
}
},
{
"href": "/door/cooler/0",
"rep": {
"openState": "Close"
}
},
{
"href": "/door/onedoorfreezer/vs/0",
"rep": {
"x.com.samsung.da.openState": "Close"
}
},
{
"href": "/doors/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Door",
"x.com.samsung.da.openState": "Close"
},
{
"x.com.samsung.da.id": "3",
"x.com.samsung.da.description": "Door",
"x.com.samsung.da.openState": "Close"
}
]
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+01:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/icemaker/status/0",
"rep": {}
},
{
"href": "/icemaker/status/vs/0",
"rep": {}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "ARTIK051_DONGLE_REF|00090741|00090028001130300100000000000000",
"x.com.samsung.da.description": "ARTIK_REF_17K",
"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": "WiFi Module",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "011120190711",
"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": "14040703,ffffffff",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/mode/0",
"rep": {
"supportedModes": [
"HOMECARE_WIZARD_V2",
"RVACATION_ON",
"RVACATION_OFF"
],
"modes": [
"RVACATION_OFF"
]
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2",
"RVACATION_ON",
"RVACATION_OFF"
],
"x.com.samsung.da.modes": [
"RVACATION_OFF"
]
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "Micom",
"x.com.samsung.da.newVersionAvailable": "false"
}
},
{
"href": "/refrigeration/0",
"rep": {
"defrost": false,
"rapidFreeze": false,
"rapidCool": false
}
},
{
"href": "/refrigeration/vs/0",
"rep": {
"x.com.samsung.da.rapidFridge": "Off"
}
},
{
"href": "/sabbath/vs/0",
"rep": {}
},
{
"href": "/temperature/current/cooler/0",
"rep": {
"range": [
1.0,
7.0
],
"units": "C",
"temperature": 3.0
}
},
{
"href": "/temperature/desired/cooler/0",
"rep": {
"range": [
1.0,
7.0
],
"units": "C",
"temperature": 3.0
}
},
{
"href": "/temperatures/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Fridge",
"x.com.samsung.da.desired": "3",
"x.com.samsung.da.current": "3",
"x.com.samsung.da.maximum": "7",
"x.com.samsung.da.minimum": "1",
"x.com.samsung.da.unit": "Celsius"
}
]
}
}
]
}
@@ -0,0 +1,184 @@
{
"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": "/bespoke/vs/0",
"rep": {
"x.com.samsung.da.BespokeProduct": "Off"
}
},
{
"href": "/defrost/delay/0",
"rep": {}
},
{
"href": "/defrost/delay/vs/0",
"rep": {}
},
{
"href": "/diagnosis/vs/0",
"rep": {
"x.com.samsung.da.diagnosisStart": "Ready"
}
},
{
"href": "/door/onedoorfreezer/vs/0",
"rep": {
"x.com.samsung.da.openState": "Close"
}
},
{
"href": "/doors/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "3",
"x.com.samsung.da.description": "Door",
"x.com.samsung.da.openState": "Close"
}
]
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+02:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/icemaker/status/0",
"rep": {}
},
{
"href": "/icemaker/status/vs/0",
"rep": {}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "ARTIK051_DONGLE_REF|00127641|00080020001430300100000000000000",
"x.com.samsung.da.description": "ARTIK_REF_17K",
"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": "WiFi Module",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "011120190711",
"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": "2103120d,ffffffff",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/mode/0",
"rep": {
"supportedModes": [
"HOMECARE_WIZARD_V2",
"ENERGY_REPORT_MODEL",
"18K_REF_OUTDOOR_CONTROL_V2"
]
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2",
"ENERGY_REPORT_MODEL",
"18K_REF_OUTDOOR_CONTROL_V2"
]
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "Micom",
"x.com.samsung.da.newVersionAvailable": "false"
}
},
{
"href": "/refrigeration/0",
"rep": {
"defrost": false,
"rapidFreeze": false,
"rapidCool": false
}
},
{
"href": "/refrigeration/vs/0",
"rep": {
"x.com.samsung.da.rapidFreezing": "Off"
}
},
{
"href": "/sabbath/vs/0",
"rep": {}
},
{
"href": "/temperature/current/freezer/0",
"rep": {
"range": [
-23.0,
-15.0
],
"units": "C",
"temperature": -20.0
}
},
{
"href": "/temperature/desired/freezer/0",
"rep": {
"range": [
-23.0,
-15.0
],
"units": "C",
"temperature": -20.0
}
},
{
"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": "-20",
"x.com.samsung.da.current": "-20",
"x.com.samsung.da.maximum": "-15",
"x.com.samsung.da.minimum": "-23",
"x.com.samsung.da.unit": "Celsius"
}
]
}
}
]
}
+170
View File
@@ -0,0 +1,170 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.timeforlongnoti": "false",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/alarms/vs/0",
"rep": {}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativePower": "915",
"x.com.samsung.da.cumulativeUnit": "Wh"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "A-VSKR-TP1-22-VS9500AL|50023541|80030100001611000800000000000000",
"x.com.samsung.da.description": "A-VSKR-TP1-22-VS9500AL",
"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": "VS1",
"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": "023741250507",
"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": "25050800",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+09:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "2650000000",
"x.com.samsung.da.airconOptionList": [
"HOMECARE_WIZARD_V2",
"CLEAN_OPTION_SUPPORT"
]
}
},
{
"href": "/component/station/dustbag/vs/0",
"rep": {
"x.com.samsung.da.status": "normal",
"x.com.samsung.da.supportedStatus": [
"full",
"normal"
]
}
},
{
"href": "/setting/dustbin/vs/0",
"rep": {
"x.com.samsung.da.autoEmpty": "On",
"x.com.samsung.da.autoClose": "On",
"x.com.samsung.da.desiredDischargingTime": "3",
"x.com.samsung.da.supportedDischargingTime": [
"1",
"3"
]
}
},
{
"href": "/status/cleanstation/vs/0",
"rep": {
"x.com.samsung.da.status": "Ready",
"x.com.samsung.da.uvcTotalOperationTime": "90",
"x.com.samsung.da.uvcOperationTime": "90",
"x.com.samsung.da.uvcFinishedTime": "2026-07-27T10:26Z",
"x.com.samsung.da.emittedTime": "2026-07-26T12:26Z",
"x.com.samsung.da.uvcIntensive": "Off",
"x.com.samsung.da.stickStatus": "On"
}
},
{
"href": "/component/station/dustbagusage/vs/0",
"rep": {
"x.com.samsung.da.dustbagUsage": "506",
"x.com.samsung.da.dustbagPrevUsage": "0"
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"flashingProgress": ""
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/connectionconfig/vs/0",
"rep": {
"autoReconnectionMinVersion": "1.0",
"autoReconnection": "true",
"autoReconnectionProtocolType": [
"helper_hotspot",
"ble_ocf"
],
"supportedWiFiAuthType": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"SAE"
],
"supportedWiFiCryptoType": [
"TKIP",
"AES",
"WEP-64",
"WEP-128"
],
"supportedWiFiFreq": [
"2.4G"
]
}
}
]
}
+407
View File
@@ -0,0 +1,407 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/alarms/vs/0",
"rep": {}
},
{
"href": "/diagnosis/vs/0",
"rep": {
"x.com.samsung.da.diagnosisStart": "Ready"
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "-500",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePower": "40500",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.cumulativeDate": "1784988000",
"x.com.samsung.da.cumulativeDateUTC": "1785013200"
}
},
{
"href": "/energy/consumption/0",
"rep": {}
},
{
"href": "/course/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2"
],
"x.com.samsung.da.options": [
"DeviceType_0145",
"UpdateAllow_NotAllowed",
"Course_01",
"LaundryOutTime_0",
"AddGarmentIndicator_Off",
"SeamlessControl_Disable",
"KidsLockBypass_On",
"WashingTimes_18",
"DrumCleanProposal_20",
"DetergentOnce_0",
"DetergentLeft_0",
"DetergentBase_0",
"DetergentAlarm_Off",
"DetergentType_0",
"DetergentTotal_0",
"SoftenerOnce_0",
"SoftenerLeft_0",
"SoftenerBase_0",
"SoftenerAlarm_Off",
"SoftenerType_0",
"SoftenerTotal_0",
"SpecialFunction_6",
"AvailableDelayTime_67",
"DetergentLevelCtrl_0",
"SoftenerLevelCtrl_3",
"SupportedDetergentLevelCtrl_00010203",
"SupportedSoftenerLevelCtrl_00010203",
"DetergentLevel2Ctrl_1",
"SoftenerLevel2Ctrl_1",
"SupportedDetergentLevel2Ctrl_010203",
"SupportedSoftenerLevel2Ctrl_010203",
"LaundryPlannerUserSetTime_0",
"ProgressTimeSet_4205DC8208E8A206AE",
"SuperSpeed_Off",
"SendToDevice_Off",
"DetergentCycleUsed_0",
"SoftenerCycleUsed_0",
"CloudCourse_001C590549164D114A224C2037F0AC22",
"CloudExtraCourse_5958",
"OneTimeCloudCourse_FFFF010049004D004A804C0037F0AC00",
"EnergyLevelSet_05020403040204040401030203",
"MostUsed_01843E913FA43BC33E",
"SuperSpeedSet_F0F000F00000000000000000",
"DispenserSet_22222222222222FFF2000022",
"GeoFenceAlarm",
"UsagesDB_ok",
"EnergyKW_396",
"DrumCleanLog_2025-09-07T18:15:52|2025-10-05T18:49:21|2025-11-23T18:20:07|2026-01-01T19:11:59|2026-02-15T03:36:21|2026-03-09T19:59:19|2026-04-19T21:35:33|2026-05-10T19:21:00|2026-06-12T21:32:16|2026-06-29T06:29:50",
"TimeSync_NotSupported"
],
"x.com.samsung.da.supportedOptions": [
"401843E913FA43BC33E53853E913FA43FC53E86843E913FA43FC33E7C853E923FA43FC53E65821E913FA43FC33E7D843E923FA31FC33E1783089102A410C3087E85209204A308C0005E8000913FA43FC00055831E913FA31FC20E57821E913FA20FC10204843E913FA43FC13E"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off"
}
},
{
"href": "/power/0",
"rep": {
"value": false
}
},
{
"href": "/cycleinterface/vs/0",
"rep": {}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/kidslock/0",
"rep": {
"value": false
}
},
{
"href": "/operational/state/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.remainingTime": "01:07: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",
"Weightsensing",
"Wash",
"Rinse",
"Spin",
"Finish"
]
}
},
{
"href": "/operational/state/0",
"rep": {
"currentMachineState": "**REDACTED**",
"machineStates": "**REDACTED**",
"jobStates": [
"None",
"Weightsensing",
"Wash",
"Rinse",
"Spin",
"Finish"
],
"currentJobState": "None",
"remainingTime": "01:07:00",
"progressPercentage": "1"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "DA_WM_TP1_21_COMMON|20233742|20000001001511200AAB020200000000",
"x.com.samsung.da.description": "DA_WM_TP1_21_COMMON_WA7700A/DC92-03005B_004A",
"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": "210",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "DA_WM_TP1_21_COMMON|20233742|20000001001511200AAB020200000000",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02986A260118(A182)",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Firmware_1_DB_20233742210931500FFFFF203005422212139101FFFF(01452023374220300542_30221213)(FileDown:0)(Type:0)",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "02337B21093150,03005B22121391",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Firmware_2_DB_2023394522081051012FFFFFFFFFFFFFFFFFFFFFFFFE(014520233945FFFFFFFF_30000000)(FileDown:0)(Type:0)",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "02339E22081051,FFFFFFFFFFFFFF"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "-07:00"
}
},
{
"href": "/washer/vs/0",
"rep": {
"x.com.samsung.da.detergentLevel": "1",
"x.com.samsung.da.softenerLevel": "2",
"x.com.samsung.da.waterTemperature": "Warm",
"x.com.samsung.da.supportedWaterTemperature": [
"None",
"Cold",
"Cool",
"EcoWarm",
"Warm",
"Hot"
],
"x.com.samsung.da.spinLevel": "High",
"x.com.samsung.da.supportedSpinLevel": [
"RinseHold",
"NoSpin",
"Low",
"Medium",
"High",
"ExtraHigh"
],
"x.com.samsung.da.soilLevel": "Normal",
"x.com.samsung.da.supportedSoilLevel": [
"None",
"ExtraLight",
"Light",
"Normal",
"Heavy",
"ExtraHeavy"
],
"x.com.samsung.da.rinseCycles": "1",
"x.com.samsung.da.supportedRinseCycles": [
"0",
"1",
"2",
"3",
"4",
"5"
]
}
},
{
"href": "/st/washercourse/vs/0",
"rep": {
"x.com.samsung.da.st.washerMode": "Table_02_Course_01",
"x.com.samsung.da.st.courseTable": "Table_02"
}
},
{
"href": "/setting/vs/0",
"rep": {}
},
{
"href": "/wm/editcourse/vs/0",
"rep": {}
},
{
"href": "/wm/setinfo/vs/0",
"rep": {
"x.com.samsung.da.isModelSettingWithoutSC": "true",
"x.com.samsung.da.isModelSettingPowerOnOff": "false",
"x.com.samsung.da.modelCode": "M(None),W(WA55A7700AV/US)"
}
},
{
"href": "/wm/jobbeginingstatus/vs/0",
"rep": {
"x.com.samsung.da.currentStatus": "None"
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"x.com.samsung.da.newVersionNo": "00000000",
"x.com.samsung.da.currentVersionInfo": "00000000",
"otnStatus": "None",
"flashingProgress": "",
"otnTarget": "main",
"otnCompleteDate": "2026-03-04",
"otnList": [
{
"type": "WIFI",
"modelId": "DA_WM_TP1_21_COMMON",
"versions": [
"30260118"
],
"visVersion": "260118"
},
{
"type": "Micom",
"modelId": "01452023374220300542",
"versions": [
"21093150",
"22121391"
],
"visVersion": "221213"
},
{
"type": "Micom",
"modelId": "014520233945FFFFFFFF",
"versions": [
"22081051",
"FFFFFFFF"
],
"visVersion": "220810"
}
]
}
},
{
"href": "/remotectrl/vs/0",
"rep": {
"x.com.samsung.da.remoteControlEnabled": "false"
}
},
{
"href": "/remotectrl/0",
"rep": {
"value": false
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "0000000000",
"x.com.samsung.da.countryCode": "US"
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "America/Phoenix",
"offset": "-07:00",
"DST": "OFF"
}
},
{
"href": "/connectionconfig/vs/0",
"rep": {
"autoReconnectionMinVersion": "1.0",
"autoReconnection": "true",
"autoReconnectionProtocolType": [
"helper_hotspot",
"ble_ocf"
],
"supportedWiFiAuthType": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"SAE"
],
"supportedWiFiCryptoType": [
"TKIP",
"AES",
"WEP-64",
"WEP-128"
],
"supportedWiFiFreq": [
"2.4G"
],
"calmConnectionCare": {
"version": "1.0",
"role": [
"things"
]
}
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
}
]
}
+314
View File
@@ -0,0 +1,314 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/alarms/vs/0",
"rep": {}
},
{
"href": "/diagnosis/vs/0",
"rep": {
"x.com.samsung.da.diagnosisStart": "Ready"
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "-500",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePower": "121100",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.cumulativeDate": "1785142800",
"x.com.samsung.da.cumulativeDateUTC": "1785110400"
}
},
{
"href": "/energy/consumption/0",
"rep": {}
},
{
"href": "/course/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2"
],
"x.com.samsung.da.options": [
"DeviceType_015C",
"UpdateAllow_NotAllowed",
"Course_01",
"LaundryOutTime_0",
"SeamlessControl_Enable",
"KidsLockBypass_On",
"FreezeProtectionAlarm_On",
"WashingTimes_19",
"DrumCleanProposal_20",
"DrumCleanLog_2026-02-04T04:05:54|2026-02-28T08:48:43|2026-03-16T03:15:29|2026-03-26T02:55:05|2026-04-15T05:57:11|2026-04-30T05:37:44|2026-05-14T05:29:20|2026-06-04T01:55:39|2026-06-25T05:21:23|2026-07-11T03:10:21",
"DetergentOnce_0",
"DetergentLeft_0",
"DetergentBase_0",
"DetergentAlarm_Off",
"DetergentType_0",
"DetergentTotal_0",
"SoftenerOnce_0",
"SoftenerLeft_0",
"SoftenerBase_0",
"SoftenerAlarm_Off",
"SoftenerType_0",
"SoftenerTotal_0",
"SpecialFunction_4",
"AvailableDelayTime_73",
"LaundryPlannerUserSetTime_0",
"ProgressTimeSet_420780820438A205BE",
"FreezeProtectionMode_Off",
"SendToDevice_Off",
"EnergyLevelSet_0503030303040304010505020204",
"MostUsed_01923FA33F510E60A17340",
"GeoFenceAlarm",
"UsagesDB_ok",
"EnergyKW_396",
"DrumCleanLog_2026-02-04T04:05:54|2026-02-28T08:48:43|2026-03-16T03:15:29|2026-03-26T02:55:05|2026-04-15T05:57:11|2026-04-30T05:37:44|2026-05-14T05:29:20|2026-06-04T01:55:39|2026-06-25T05:21:23|2026-07-11T03:10:21",
"TimeSync_NotSupported"
],
"x.com.samsung.da.supportedOptions": [
"501923FA33F510E60A1734050923FA33F510E60A173400F933FA33F510E60A173407B923FA31F520E60A1724006933FA31F510E68A172400C933FA31F520E60A172404F9204A41052046666733308913FA33F510E60A170004A923FA31F520C645175554B923FA31F520C64517555269204A102510664A171114C913FA31F510E624171404D9204A410520466667555"
]
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off"
}
},
{
"href": "/power/0",
"rep": {
"value": false
}
},
{
"href": "/cycleinterface/vs/0",
"rep": {}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/kidslock/0",
"rep": {
"value": false
}
},
{
"href": "/operational/state/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.remainingTime": "01:13: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",
"Weightsensing",
"Wash",
"Rinse",
"Spin",
"Finish"
]
}
},
{
"href": "/operational/state/0",
"rep": {
"currentMachineState": "**REDACTED**",
"machineStates": "**REDACTED**",
"jobStates": [
"None",
"Weightsensing",
"Wash",
"Rinse",
"Spin",
"Finish"
],
"currentJobState": "None",
"remainingTime": "01:13:00",
"progressPercentage": "1"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "DA_WM_TP2_20_COMMON|20233741|200000010014110002A3020200000000",
"x.com.samsung.da.description": "DA_WM_TP2_20_COMMON_WA8000T/DC92-02810A_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": "210",
"x.com.samsung.da.diagMinVersion": "1.0",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "DA_WM_TP2_20_COMMON|20233741|200000010014110002A3020200000000",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02674A250414(F546)",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Firmware_1_DB_20233741181128160FFFFF202810412011303403FFFF(015C2023374120281041_30000000)(FileDown:0)(Type:0)",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "18112816,20113034",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "Firmware_2_DB_2023394400000000032FFFFFFFFFFFFFFFFFFFFFFFFE(015C20233944FFFFFFFF_30000000)(FileDown:0)(Type:0)",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "00000000,FFFFFFFF"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+09:00"
}
},
{
"href": "/washer/vs/0",
"rep": {
"x.com.samsung.da.spinLevel": "Medium",
"x.com.samsung.da.supportedSpinLevel": [
"NoSpin",
"Delicate",
"Low",
"Medium",
"High",
"ExtraHigh"
],
"x.com.samsung.da.rinseCycles": "2",
"x.com.samsung.da.supportedRinseCycles": [
"0",
"1",
"2",
"3",
"4",
"5"
],
"x.com.samsung.da.waterValve": "Cold",
"x.com.samsung.da.supportedWaterValve": [
"None",
"Cold",
"Warm",
"Hot"
],
"x.com.samsung.da.waterHeight": "0",
"x.com.samsung.da.supportedWaterHeight": [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"NoDisplay"
],
"x.com.samsung.da.washTime": "21",
"x.com.samsung.da.supportedWashTime": [
"0",
"9",
"15",
"21",
"27",
"50+"
]
}
},
{
"href": "/st/washercourse/vs/0",
"rep": {
"x.com.samsung.da.st.washerMode": "Table_02_Course_01",
"x.com.samsung.da.st.courseTable": "Table_02"
}
},
{
"href": "/setting/vs/0",
"rep": {}
},
{
"href": "/wm/editcourse/vs/0",
"rep": {}
},
{
"href": "/wm/setinfo/vs/0",
"rep": {
"x.com.samsung.da.isModelSettingWithoutSC": "true",
"x.com.samsung.da.isModelSettingPowerOnOff": "false"
}
},
{
"href": "/wm/jobbeginingstatus/vs/0",
"rep": {
"x.com.samsung.da.currentStatus": "None"
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "Micom",
"x.com.samsung.da.newVersionAvailable": "false"
}
},
{
"href": "/remotectrl/vs/0",
"rep": {
"x.com.samsung.da.remoteControlEnabled": "false"
}
},
{
"href": "/remotectrl/0",
"rep": {
"value": false
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "4137000000",
"x.com.samsung.da.countryCode": "KR"
}
}
]
}
+295
View File
@@ -0,0 +1,295 @@
{
"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": "/selfcheck/vs/0",
"rep": {
"x.com.samsung.da.status": "Ready",
"x.com.samsung.da.result": "Success",
"x.com.samsung.da.error": [
"DA_ERROR_NONE"
],
"x.com.samsung.da.supportedActions": [
"Start"
]
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2"
],
"x.com.samsung.da.modes": [
"WATERFILTER_DISABLE"
]
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP2X_WATERPURIFIER_20K|00156641|900000000216130001088700000E0000",
"x.com.samsung.da.description": "TP2X_WATERPURIFIER_20K",
"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": "RP0",
"x.com.samsung.da.diagMinVersion": "1.0",
"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": "02144A240904",
"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": "24091109, 2409110D, 22092302, FFFFFFFF, 23030200, FFFFFFFF",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+00:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"otnStatus": "None",
"flashingProgress": "",
"otnList": [
{
"type": "WIFI",
"modelId": "A-WPWW-TP2-22-COMMON",
"versions": [
"20240904"
],
"visVersion": "240904"
},
{
"type": "Micom",
"modelId": "03800015664100156041",
"versions": [
"24091109",
"2409110D"
],
"visVersion": "240911"
},
{
"type": "Micom",
"modelId": "038000156141FFFFFFFF",
"versions": [
"22092302",
"FFFFFFFF"
],
"visVersion": "220923"
},
{
"type": "Micom",
"modelId": "038000159141FFFFFFFF",
"versions": [
"23030200",
"FFFFFFFF"
],
"visVersion": "230302"
}
]
}
},
{
"href": "/filter/waterfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "90",
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterStatus": "normal",
"x.com.samsung.da.lastResetDate": "2025-08-29T03:22:31",
"x.com.samsung.da.filterResetType": [
"replaceable"
]
}
},
{
"href": "/status/lock/vs/0",
"rep": {
"x.com.samsung.da.coldwaterLock": "Unlocked",
"x.com.samsung.da.buzzLock": "Unlocked"
}
},
{
"href": "/setting/waterpurifier/vs/0",
"rep": {
"x.com.samsung.da.hotwaterLevel": "5",
"x.com.samsung.da.desiredType": "ambientwater",
"x.com.samsung.da.tempDesiredHotWater": "40",
"x.com.samsung.da.tempUnit": "C",
"x.com.samsung.da.desiredCapacity": "100",
"x.com.samsung.da.capacityUnit": "C",
"x.com.samsung.da.capacityResolution": "10",
"x.com.samsung.da.hotwaterRange": [
"40",
"90"
],
"x.com.samsung.da.desiredCapacityRange": [
"50",
"2000"
],
"x.com.samsung.da.supportedTypes": [
"ambientwater",
"coldwater",
"hotwater"
],
"x.com.samsung.da.pourStatus": "Off"
}
},
{
"href": "/status/waterpurifier/vs/0",
"rep": {
"x.com.samsung.da.status": "Ready",
"x.com.samsung.da.filterDoorStatus": "Close",
"x.com.samsung.da.sterilizeRunTime": "0",
"x.com.samsung.da.sterilizeStartTime": "2026-07-27T17:00:00",
"x.com.samsung.da.sterilizePeriod": "3",
"x.com.samsung.da.sterilizePlanTime": "2026-07-27T17:00:00",
"x.com.samsung.da.sterilizeLastTime": "2026-07-24T17:08:25",
"x.com.samsung.da.sterilizeMaxTime": "10",
"x.com.samsung.da.filterCleanRemainTime": "0",
"x.com.samsung.da.filterCleanMaxTime": "14"
}
},
{
"href": "/favorite/capacity/vs/0",
"rep": {
"x.com.samsung.da.switchCapacity": "On",
"x.com.samsung.da.defaultCapacity": "100",
"x.com.samsung.da.capacityList": [
"100",
"200",
"260",
"500",
"9999"
]
}
},
{
"href": "/favorite/hotwater/vs/0",
"rep": {
"x.com.samsung.da.switchHotwater": "Unlocked",
"x.com.samsung.da.favorite.defaultTemperature": "40",
"x.com.samsung.da.favorite.revision": "0",
"x.com.samsung.da.favorite.showList": [
"40",
"75",
"85",
"90"
],
"x.com.samsung.da.favorite.supportedList": [
"40",
"75",
"85",
"90"
]
}
},
{
"href": "/automation/waterpurifier/vs/0",
"rep": {
"automation.supported.modes": [
"0"
],
"automation.supported.options": [
"capacity"
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/recipe/coffee/vs/0",
"rep": {
"publisher.support": [
"1",
"2",
"3",
"4",
"5"
]
}
},
{
"href": "/coffee/custom/recipe/vs/0",
"rep": {
"publisher.support": [
"1",
"2",
"3",
"4",
"5"
]
}
},
{
"href": "/favorite/coffee/vs/0",
"rep": {
"favorite.activate": "On",
"brew.status": "Suspend",
"favorite.enable.list": [
"0",
"1",
"2"
]
}
},
{
"href": "/recipe/coffee/deletion/vs/0",
"rep": {}
},
{
"href": "/brand/recipe/info/vs/0",
"rep": {
"revision": "0",
"brand.num.max": "1"
}
}
]
}

Some files were not shown because too many files have changed in this diff Show More