Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
498da49817 | ||
|
|
bbf3f3f833 | ||
|
|
91b129282b | ||
|
|
34fe991858 | ||
|
|
6d185e4e3d | ||
|
|
5f47fc0477 | ||
|
|
cfa82e8853 | ||
|
|
739881de16 | ||
|
|
6080d37f7d | ||
|
|
71e026b804 | ||
|
|
65b0326d62 | ||
|
|
6007505e6b | ||
|
|
5a73a25005 | ||
|
|
dc3e1255d1 | ||
|
|
daa7546208 | ||
|
|
e568145deb | ||
|
|
fcd34f73e5 | ||
|
|
a3d9cce164 | ||
|
|
defeffd58d | ||
|
|
053e15bba6 | ||
|
|
aa9cdd83b7 | ||
|
|
57e06c5ccb | ||
|
|
5c962801b4 | ||
|
|
c8597532b0 | ||
|
|
00c1d78373 | ||
|
|
1cdad7f84c | ||
|
|
1fb27c30ae | ||
|
|
123455a873 | ||
|
|
f5be651430 | ||
|
|
1bbecfa5c3 | ||
|
|
6b28272647 | ||
|
|
55c7b88a8f | ||
|
|
3032b0c032 | ||
|
|
5b30099c42 | ||
|
|
75f2be7aaf | ||
|
|
b18500dec3 | ||
|
|
51fa1c5ea8 | ||
|
|
3da01f80d0 |
@@ -6,6 +6,7 @@ description: >-
|
||||
raises the "incomplete capability coverage" repair, a diagnostics JSON needs
|
||||
triaging, or you're mapping OCF resources to HA entities. Covers reading dumps,
|
||||
OCF-standard vs vendor hrefs, the diagnostic/config/normal entity taxonomy,
|
||||
preferring dynamic (device-reported) select options over hardcoded lists,
|
||||
ensuring every href is bound or ignored, and locking it in with a fixture +
|
||||
golden + test.
|
||||
---
|
||||
@@ -103,7 +104,37 @@ sub-polled between summary polls. Pick descriptor types from `entities.py`
|
||||
as a gap for a human, or ignore it with a documented reason — never invent an
|
||||
entity on a hunch (`ignored.py`'s rule).
|
||||
|
||||
## 5. Names and enum labels live in translations, never in Python
|
||||
## 5. Select options: read them from the device, don't hardcode
|
||||
|
||||
A `SelectDesc`'s `options` should come from the device's own advertised list
|
||||
whenever the resource carries one, not from a Python tuple typed in from a
|
||||
single dump. Two dynamic forms already exist in the repo and should be
|
||||
reached for first:
|
||||
- `options_field='x.com.samsung.da.supportedModes'` (or whatever the
|
||||
resource's own supported-values field is called) — reads the live rep on
|
||||
the capability's own href. See `laundry.py`'s `buzzer_sound`/
|
||||
`finish_sound` (`options_field='supportedBuzzerSound'`/
|
||||
`'supportedFinishSound'`).
|
||||
- `options=<callable>` — for option lists that live on a **different**
|
||||
resource than the select's own href (e.g. a course table keyed off a
|
||||
sibling href). See `laundry.cycle_select`'s `options=cycle_options`.
|
||||
|
||||
A static `options=(...)` tuple is a coverage gap waiting to happen: the next
|
||||
dump from a different board generation will report modes/values the tuple
|
||||
doesn't have, and both the HA options list *and* `write_fn`'s validation (if
|
||||
it checks the same tuple) will silently reject values the device itself
|
||||
advertises as supported. That's exactly what happened with `oven._OVEN_MODES`
|
||||
in issue #138 — a hardcoded list rejected `AirFryer`/`Dehydrate`/
|
||||
`SelfClean`/etc. even though the device's own `supportedModes` field listed
|
||||
them. Reach for a static tuple only when the dump genuinely has no
|
||||
supported-values field to read (e.g. the NV7000BS-class oven dump
|
||||
`_OVEN_MODES` was inferred before any live oven dump existed — see that
|
||||
module's docstring), and treat it as an interim best-guess rather than a
|
||||
permanent design choice: migrate it to `options_field`/a callable the moment
|
||||
a dump with a real supported-values list surfaces, instead of just adding
|
||||
the new values to the static tuple.
|
||||
|
||||
## 6. Names and enum labels live in translations, never in Python
|
||||
|
||||
Descriptors have **no `name` field**. Every entity is named from the shipped
|
||||
catalog, keyed by `translation_key` — which defaults to the descriptor's own
|
||||
@@ -142,7 +173,7 @@ no `[%key:...%]` resolution (that's Core build tooling). Every other language
|
||||
must mirror `en.json` key for key — also enforced by
|
||||
`tests/test_translations.py`.
|
||||
|
||||
## 6. Coverage discipline: bound or ignored
|
||||
## 7. Coverage discipline: bound or ignored
|
||||
|
||||
Every href in the dump must resolve, or the repair fires. If a resource isn't
|
||||
worth an entity, add it to `capabilities/ignored.py` (a no-entity `Capability`)
|
||||
@@ -156,7 +187,7 @@ friendlier href**.
|
||||
ignored because washers bind it. When only one family should ignore an href
|
||||
that another binds, scope the ignore to that family's registry.
|
||||
|
||||
## 7. Reuse before writing new code
|
||||
## 8. Reuse before writing new code
|
||||
|
||||
Check `common.py` (generic OCF: power, energy, alarms, water) and `laundry.py`
|
||||
(shared washer/dryer/dishwasher: buzzer, job status, `cycle_select` + course
|
||||
@@ -165,7 +196,7 @@ registry uses `fridge.FIRMWARE_UPDATE`; all three laundry families share
|
||||
`laundry.cycle_select`. If two families hand-roll the same helper, hoist it to a
|
||||
shared module rather than copying.
|
||||
|
||||
## 8. Lock it in
|
||||
## 9. Lock it in
|
||||
|
||||
1. Add a **scrubbed** fixture `tests/fixtures/<type>_device.json`
|
||||
(`{"device0": [ {devcol rep}, {href, rep}, ... ]}`) — replace serials, MACs,
|
||||
|
||||
@@ -3,6 +3,19 @@
|
||||
<!-- light mode -->
|
||||
<img src="custom_components/localthings/brand/logo@2x.png#gh-light-mode-only" alt="LocalThings Logo"/>
|
||||
|
||||
<p align="center">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/mbillow/localthings" />
|
||||
<img alt="GitHub watchers" src="https://img.shields.io/github/watchers/mbillow/localthings" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="GitHub Release" src="https://img.shields.io/github/v/release/mbillow/localthings" />
|
||||
<img alt="hacs validation" src="https://img.shields.io/github/check-runs/mbillow/localthings/main?nameFilter=HACS%20validation&label=hacs%20validation" />
|
||||
<img alt="hassfest" src="https://img.shields.io/github/check-runs/mbillow/localthings/main?nameFilter=Hassfest%20validation&label=hassfest" />
|
||||
<img alt="tests" src="https://img.shields.io/github/check-runs/mbillow/localthings/main?nameFilter=Pytest&label=tests" />
|
||||
|
||||
</p>
|
||||
|
||||
# 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.
|
||||
@@ -35,6 +48,7 @@ Your state stays on your LAN: HA talks to the appliance over a direct DTLS sessi
|
||||
| Washer | `by_type/washer.py` |
|
||||
| Water purifier | `by_type/water_purifier.py` |
|
||||
| Vacuum clean/auto-empty station | `by_type/vacuum_station.py` |
|
||||
| Air dresser | `by_type/air_dresser.py` |
|
||||
|
||||
Each registry composes shared and family-specific `Capability` objects from `registry/capabilities/`; those modules document the individual resources/entities in more depth than a README table can stay current with.
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ from .registry.capabilities.airconditioner import (
|
||||
HREF_WIND_DIRECTION as WIND_DIRECTION_HREF,
|
||||
HREF_WIND_OSCILLATION as WIND_OSCILLATION_HREF,
|
||||
HREF_CONVENIENT as CONVENIENT_HREF,
|
||||
HREF_AIRFLOW as AIRFLOW_HREF,
|
||||
is_legacy_board,
|
||||
)
|
||||
from .registry.capabilities.common import normalize_temp_unit
|
||||
|
||||
@@ -141,6 +143,22 @@ def _oscillation_swing(rep: dict) -> str | None:
|
||||
return 'horizontal'
|
||||
return 'off'
|
||||
|
||||
def _wind_strength_label(code, rep: dict) -> str:
|
||||
"""Human label for a /wind/strength/vs/0 code from the device's own
|
||||
modesName array (parallel-indexed with supportedModes), lowercased for
|
||||
HA -- used only for codes _DEVICE_TO_FAN doesn't already cover (issue
|
||||
#155, TP1X_DA-AC-RAC-01001_0000: codes "0"/"31"-"35" instead of the
|
||||
"0"-"4" scale _DEVICE_TO_FAN was built from, with modesName giving
|
||||
"Auto"/"1"/"2"/"3"/"4"/"MAX"). No per-model numeric map -- mirrors
|
||||
preset_mode's dynamic code->str resolution. Falls back to the raw code
|
||||
lowercased when modesName is absent or misaligned."""
|
||||
supported = rep.get('x.com.samsung.da.supportedModes') or []
|
||||
names = rep.get('x.com.samsung.da.modesName') or []
|
||||
if code in supported and len(names) == len(supported):
|
||||
return str(names[supported.index(code)]).lower()
|
||||
return str(code).lower()
|
||||
|
||||
|
||||
# Preset (convenient mode): resolved dynamically from the device's own
|
||||
# /mode/convenient/vs/0 supportedModes -- no per-model table. The device 'Off'
|
||||
# code maps to HA's PRESET_NONE ("no preset active"); every other code is
|
||||
@@ -228,8 +246,57 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
|
||||
# -- resource helpers ---------------------------------------------------
|
||||
|
||||
# Preset codes on legacy ARTIK051 boards, learned by driving the same unit
|
||||
# through its cloud integration and reading the local token back each time:
|
||||
# Nano=windFree, Quiet, Comfort, 2Step, Speed=Fast Turbo, Off=none.
|
||||
_LEGACY_PRESET_CODES = ('Off', 'Nano', 'Quiet', 'Comfort', '2Step', 'Speed')
|
||||
|
||||
def _legacy_convenient(self) -> dict:
|
||||
"""A /mode/convenient/vs/0-shaped rep built from the Comode_* token in
|
||||
/mode/vs/0's options, for boards that have no convenient resource."""
|
||||
options = (self.coordinator.resource(MODE_HREF) or {}).get(
|
||||
'x.com.samsung.da.options') or []
|
||||
for option in options:
|
||||
if isinstance(option, str) and option.startswith('Comode_'):
|
||||
return {_MODES_FIELD: [option.split('_', 1)[1]],
|
||||
_SUPPORTED_FIELD: list(self._LEGACY_PRESET_CODES)}
|
||||
return {}
|
||||
|
||||
def _legacy_airflow(self) -> dict:
|
||||
"""The /airflow/vs/0 rep, but only when it is the fan/swing channel to
|
||||
use -- i.e. this board has no /wind/strength/vs/0.
|
||||
|
||||
Delegates the board-generation test to is_legacy_board (the same
|
||||
test capabilities/airconditioner.py's token entities are gated on)
|
||||
instead of re-implementing it. Uses last_resources rather than a
|
||||
two-key presence dict built from coordinator.resource()'s truthiness
|
||||
-- resource() collapses "href absent" and "href present with an
|
||||
empty {} rep" to the same falsy value, while is_legacy_board (and
|
||||
discover()'s own binding) test key membership, not truthiness. A
|
||||
presence dict built from truthiness alone would disagree with the
|
||||
token entities on a board reporting a genuinely empty /airflow/vs/0,
|
||||
silently reintroducing the drift this delegation exists to prevent.
|
||||
"""
|
||||
if not is_legacy_board(self.coordinator.last_resources):
|
||||
return {}
|
||||
return self.coordinator.resource(AIRFLOW_HREF) or {}
|
||||
|
||||
def _legacy_preset(self) -> bool:
|
||||
"""Whether presets come from the Comode_* token rather than a resource.
|
||||
|
||||
Gated on the same board test as _legacy_airflow, not on the convenient
|
||||
rep being empty alone: newer boards carry Comode tokens too, so a
|
||||
momentarily empty /mode/convenient/vs/0 there must not silently switch
|
||||
the preset read (and write) over to the token path.
|
||||
"""
|
||||
return (not self.coordinator.resource(CONVENIENT_HREF)
|
||||
and bool(self._legacy_airflow()))
|
||||
|
||||
def _rep(self, href: str) -> dict:
|
||||
return self.coordinator.resource(href) or {}
|
||||
rep = self.coordinator.resource(href) or {}
|
||||
if not rep and href == CONVENIENT_HREF and self._legacy_airflow():
|
||||
return self._legacy_convenient()
|
||||
return rep
|
||||
|
||||
def _is_on(self) -> bool:
|
||||
# Prefer the vendor /power/vs/0 (present on every observed board and
|
||||
@@ -373,11 +440,27 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
return self._read_mode(WIND_STRENGTH_HREF, _DEVICE_TO_FAN)
|
||||
airflow = self._legacy_airflow()
|
||||
if airflow:
|
||||
return _DEVICE_TO_FAN.get(str(airflow.get('x.com.samsung.da.speedLevel')))
|
||||
rep = self._rep(WIND_STRENGTH_HREF)
|
||||
code = _first(rep.get(_MODES_FIELD))
|
||||
if code is None:
|
||||
return None
|
||||
return _DEVICE_TO_FAN.get(code) or _wind_strength_label(code, rep)
|
||||
|
||||
@property
|
||||
def fan_modes(self) -> list[str]:
|
||||
return self._read_modes(WIND_STRENGTH_HREF, _DEVICE_TO_FAN)
|
||||
if self._legacy_airflow():
|
||||
# This resource carries no supportedModes, so the full scale is offered.
|
||||
return list(_DEVICE_TO_FAN.values())
|
||||
rep = self._rep(WIND_STRENGTH_HREF)
|
||||
modes = []
|
||||
for code in self._supported(WIND_STRENGTH_HREF):
|
||||
mode = _DEVICE_TO_FAN.get(code) or _wind_strength_label(code, rep)
|
||||
if mode not in modes:
|
||||
modes.append(mode)
|
||||
return modes
|
||||
|
||||
def _swing_via_direction(self) -> bool:
|
||||
"""True when WIND_DIRECTION_HREF is the swing channel to use --
|
||||
@@ -387,12 +470,17 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
def swing_mode(self):
|
||||
airflow = self._legacy_airflow()
|
||||
if airflow:
|
||||
return _DEVICE_TO_SWING.get(airflow.get('x.com.samsung.da.direction'))
|
||||
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]:
|
||||
if self._legacy_airflow():
|
||||
return list(_SWING_TO_DEVICE.keys())
|
||||
if self._swing_via_direction():
|
||||
return self._read_modes(WIND_DIRECTION_HREF, _DEVICE_TO_SWING)
|
||||
if self._rep(WIND_OSCILLATION_HREF):
|
||||
@@ -468,9 +556,37 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
await self.coordinator.async_send_command(self._bound, (kind, device))
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
await self._set_mapped('fan', _FAN_TO_DEVICE, fan_mode)
|
||||
if self._legacy_airflow():
|
||||
level = _FAN_TO_DEVICE.get(fan_mode)
|
||||
if level is not None:
|
||||
await self.coordinator.async_send_command(
|
||||
self._bound, ('fan_legacy', level))
|
||||
return
|
||||
supported = self._supported(WIND_STRENGTH_HREF)
|
||||
device = _FAN_TO_DEVICE.get(fan_mode)
|
||||
# A static hit is only trustworthy if this unit's own supportedModes
|
||||
# actually includes that code -- a board can use non-standard codes
|
||||
# (issue #155's "31"-"35") while still spelling a standard label
|
||||
# ("Low"/"High") in modesName, in which case _FAN_TO_DEVICE.get would
|
||||
# return a plausible-looking code ('1'/'3') the device never
|
||||
# advertised at all. Fall through to the live scan whenever the
|
||||
# static guess isn't actually one of this unit's own codes.
|
||||
if device is None or (supported and device not in supported):
|
||||
rep = self._rep(WIND_STRENGTH_HREF)
|
||||
for code in supported:
|
||||
if _wind_strength_label(code, rep) == fan_mode:
|
||||
device = code
|
||||
break
|
||||
if device is not None:
|
||||
await self.coordinator.async_send_command(self._bound, ('fan', device))
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
if self._legacy_airflow():
|
||||
code = _SWING_TO_DEVICE.get(swing_mode)
|
||||
if code is not None:
|
||||
await self.coordinator.async_send_command(
|
||||
self._bound, ('swing_legacy', code))
|
||||
return
|
||||
if self._swing_via_direction():
|
||||
await self._set_mapped('swing', _SWING_TO_DEVICE, swing_mode)
|
||||
return
|
||||
@@ -489,5 +605,6 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
# 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))
|
||||
kind = 'preset_legacy' if self._legacy_preset() else 'preset'
|
||||
await self.coordinator.async_send_command(self._bound, (kind, code))
|
||||
return
|
||||
|
||||
@@ -366,14 +366,22 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
if reg is None:
|
||||
reg = for_device_by_resources(resources)
|
||||
unbound: list[str] = []
|
||||
hot, warm = set(), set()
|
||||
|
||||
def _tier_log(href: str, tier: str) -> None:
|
||||
if tier == 'hot':
|
||||
hot.add(href)
|
||||
elif tier == 'warm':
|
||||
warm.add(href)
|
||||
|
||||
if reg is not None:
|
||||
self._log.debug("device type: %s (oneUiVersion=%r)", reg.name, one_ui)
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities,
|
||||
log=unbound.append)
|
||||
log=unbound.append, tier_log=_tier_log)
|
||||
self.device_type_name = reg.name
|
||||
else:
|
||||
self._log.warning("unknown device type oneUiVersion=%r; using common caps", one_ui)
|
||||
bound = discover(resources, CAPABILITIES, log=unbound.append)
|
||||
bound = discover(resources, CAPABILITIES, log=unbound.append, tier_log=_tier_log)
|
||||
self.device_type_name = None
|
||||
self.bound = bound
|
||||
self._unbound_hrefs = unbound
|
||||
@@ -398,13 +406,6 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
)
|
||||
self._update_coverage_gap_issue(reg is None, unbound, name)
|
||||
|
||||
hot, warm = set(), set()
|
||||
for be in bound:
|
||||
tier = be.capability.poll_tier
|
||||
if tier == 'hot':
|
||||
hot.add(be.href)
|
||||
elif tier == 'warm':
|
||||
warm.add(be.href)
|
||||
self._hot_hrefs = sorted(hot)
|
||||
self._warm_hrefs = sorted(warm)
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
"""Fan platform for Samsung range hoods and air purifiers.
|
||||
|
||||
Three FanDesc-bound hrefs exist, dispatched by href in async_setup_entry
|
||||
Four FanDesc-bound hrefs exist, dispatched by href in async_setup_entry
|
||||
below since each needs different HA fan semantics: the range hood's fan
|
||||
speed and the older ARTIK051_TVTL air-purifier family's Auto/Sleep/Low/
|
||||
Medium/High (issue #56) are both an ordered set of numeric levels
|
||||
(SET_SPEED) -- the latter confirmed monotonic in capabilities/
|
||||
air_purifier.py's module docstring, with no named-mode list to preserve
|
||||
since this board never self-reports one. The newer TP1X air-purifier
|
||||
family's modes (Smart/Max/Mid/WindFree/Sleep, issue #130) are named
|
||||
behaviors with no linear order (PRESET_MODE), reported directly by that
|
||||
board's own supportedModes."""
|
||||
since this board never self-reports one. The TP1X air-purifier family's
|
||||
modes (Smart/Max/Mid/WindFree/Sleep, issue #130) and the A-VTWW-TP2-21
|
||||
family's /wind/strength/vs/0 modes (issue #151) are both named behaviors
|
||||
with no linear order (PRESET_MODE) -- LocalThingsAirPurifierFan handles
|
||||
both hrefs, the only difference being whether the label comes straight
|
||||
from supportedModes or from a parallel modesName array (see
|
||||
_label_for_code)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -29,6 +32,7 @@ from .coordinator import LocalThingsCoordinator
|
||||
from .entity import LocalThingsEntity, _is_included
|
||||
from .registry.capabilities.air_purifier import HREF_AIRFLOW
|
||||
from .registry.capabilities.air_purifier import HREF_MODE as AIR_PURIFIER_FAN_HREF
|
||||
from .registry.capabilities.air_purifier import HREF_WIND_STRENGTH as AIR_PURIFIER_WIND_STRENGTH_HREF
|
||||
from .registry.entities import FanDesc
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -42,6 +46,7 @@ _OFF_SPEED_CODE = '0'
|
||||
|
||||
_MODES_FIELD = 'x.com.samsung.da.modes'
|
||||
_SUPPORTED_MODES_FIELD = 'x.com.samsung.da.supportedModes'
|
||||
_MODES_NAME_FIELD = 'x.com.samsung.da.modesName'
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -54,7 +59,7 @@ async def async_setup_entry(
|
||||
for bound in coordinator.bound:
|
||||
if not (isinstance(bound.desc, FanDesc) and _is_included(bound, coordinator)):
|
||||
continue
|
||||
if bound.href == AIR_PURIFIER_FAN_HREF:
|
||||
if bound.href in (AIR_PURIFIER_FAN_HREF, AIR_PURIFIER_WIND_STRENGTH_HREF):
|
||||
entities.append(LocalThingsAirPurifierFan(coordinator, bound))
|
||||
elif bound.href == HREF_AIRFLOW:
|
||||
entities.append(LocalThingsAirflowFan(coordinator, bound))
|
||||
@@ -243,10 +248,29 @@ class LocalThingsAirPurifierFan(LocalThingsEntity, FanEntity):
|
||||
return str(power).lower() == 'on'
|
||||
return bool(self._rep(POWER_HREF).get('value'))
|
||||
|
||||
def _label_for_code(self, code) -> str:
|
||||
"""Lowercased HA preset label for a device mode code.
|
||||
|
||||
The TP1X_DA-AC-AIR board (issue #130) reports its named modes
|
||||
directly as supportedModes ('Smart'/'Max'/...), so the code IS the
|
||||
label. The A-VTWW-TP2-21 board (issue #151) instead reports numeric
|
||||
wind-strength codes ('87'/'89'/...) with a separate modesName array
|
||||
(parallel-indexed with supportedModes) giving the actual names --
|
||||
same shape as climate.py's _wind_strength_label, and coincidentally
|
||||
the same word set (Smart/Max/WindFree/Sleep), so both board
|
||||
generations land on identical HA preset values without needing
|
||||
their own translation catalog entry."""
|
||||
rep = self._mode_rep()
|
||||
supported = list(rep.get(_SUPPORTED_MODES_FIELD, ()))
|
||||
names = rep.get(_MODES_NAME_FIELD)
|
||||
if names and code in supported and len(names) == len(supported):
|
||||
return str(names[supported.index(code)]).lower()
|
||||
return str(code).lower()
|
||||
|
||||
@property
|
||||
def preset_modes(self) -> list[str]:
|
||||
return [
|
||||
str(code).lower()
|
||||
self._label_for_code(code)
|
||||
for code in self._mode_rep().get(_SUPPORTED_MODES_FIELD, ())
|
||||
]
|
||||
|
||||
@@ -254,7 +278,7 @@ class LocalThingsAirPurifierFan(LocalThingsEntity, FanEntity):
|
||||
def preset_mode(self) -> str | None:
|
||||
modes = self._mode_rep().get(_MODES_FIELD)
|
||||
code = modes[0] if isinstance(modes, (list, tuple)) and modes else modes
|
||||
return str(code).lower() if code is not None else None
|
||||
return self._label_for_code(code) if code is not None else None
|
||||
|
||||
async def async_turn_on(
|
||||
self, percentage: int | None = None, preset_mode: str | None = None,
|
||||
@@ -269,10 +293,10 @@ class LocalThingsAirPurifierFan(LocalThingsEntity, FanEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
# Reverse-resolve against the unit's own supportedModes -- the
|
||||
# write needs the raw device code (e.g. 'WindFree'), not the
|
||||
# lowercased HA value.
|
||||
# write needs the raw device code (e.g. 'WindFree', or '90' on the
|
||||
# modesName-labelled board), not the lowercased HA value.
|
||||
for code in self._mode_rep().get(_SUPPORTED_MODES_FIELD, ()):
|
||||
if str(code).lower() == preset_mode:
|
||||
if self._label_for_code(code) == preset_mode:
|
||||
await self.coordinator.async_send_command(self._bound, ('mode', code))
|
||||
return
|
||||
_LOGGER.warning(
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"entity": {
|
||||
"climate": {
|
||||
"airconditioner": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"1": "mdi:fan-speed-1",
|
||||
"2": "mdi:fan-speed-1",
|
||||
"3": "mdi:fan-speed-2",
|
||||
"4": "mdi:fan-speed-3",
|
||||
"5": "mdi:fan-speed-3",
|
||||
"turbo": "mdi:fan-speed-3",
|
||||
"max": "mdi:fan-speed-3"
|
||||
}
|
||||
},
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"ai_comfort": "mdi:creation",
|
||||
"quiet": "mdi:volume-off",
|
||||
"smart": "mdi:brain",
|
||||
"speed": "mdi:speedometer",
|
||||
"nano": "mdi:weather-dust",
|
||||
"nanosleep": "mdi:sleep",
|
||||
"longwind": "mdi:weather-windy",
|
||||
"motiondirect": "mdi:account-arrow-left",
|
||||
"motionindirect": "mdi:account-arrow-right",
|
||||
"drycomfort": "mdi:water-percent",
|
||||
"2step": "mdi:stairs"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"air_purifier_fan": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"smart": "mdi:brain",
|
||||
"max": "mdi:fan-speed-3",
|
||||
"mid": "mdi:fan-speed-2",
|
||||
"windfree": "mdi:weather-dust",
|
||||
"sleep": "mdi:sleep"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"machine_state": {
|
||||
"state": {
|
||||
"idle": "mdi:power-standby",
|
||||
"active": "mdi:play",
|
||||
"pause": "mdi:pause"
|
||||
}
|
||||
},
|
||||
"connection_mode": {
|
||||
"state": {
|
||||
"observe": "mdi:broadcast",
|
||||
"poll": "mdi:sync"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,5 +12,5 @@
|
||||
"pyOpenSSL>=23.0",
|
||||
"smartthings-local>=0.1.1"
|
||||
],
|
||||
"version": "0.14.0"
|
||||
"version": "0.15.0"
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ from typing import Optional
|
||||
|
||||
from ._base import DeviceRegistry
|
||||
from . import (
|
||||
air_purifier, airconditioner, cooktop, dehumidifier, dishwasher, dryer,
|
||||
induction_cooktop, microwave, oven, range as _range, range_hood,
|
||||
refrigerator, vacuum_station, washer, water_purifier,
|
||||
air_dresser, air_purifier, airconditioner, cooktop, dehumidifier,
|
||||
dishwasher, dryer, induction_cooktop, microwave, oven, range as _range,
|
||||
range_hood, refrigerator, vacuum_station, washer, water_purifier,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -15,6 +15,7 @@ __all__ = [
|
||||
|
||||
|
||||
_REGISTRY_BY_KEY: dict[str, DeviceRegistry] = {
|
||||
'air_dresser': air_dresser.REGISTRY,
|
||||
'air_purifier': air_purifier.REGISTRY,
|
||||
'airpurifier': air_purifier.REGISTRY,
|
||||
'airconditioner': airconditioner.REGISTRY,
|
||||
@@ -200,6 +201,13 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
||||
# here) -- no WAC-specific resources needed.
|
||||
if key is None and '_WAC_' in (model_num or ''):
|
||||
key = 'airconditioner'
|
||||
# Wind-Free 2-in-1 systems (floor-standing + wall-mounted indoor units
|
||||
# sharing one outdoor unit and one local IP, e.g. TP2X_FAC_BORA_21K,
|
||||
# issues #150/#153) report no oneUiVersion and carry the '_FAC_' token
|
||||
# instead of '_RAC_'/'_PRAC_'. Same TP1X/TP2X-class resource surface as
|
||||
# the room-AC models above.
|
||||
if key is None and '_FAC_' in (model_num or ''):
|
||||
key = 'airconditioner'
|
||||
# ARA-WW-class wall-mount RACs (e.g. ARA-WW-TP1-22-COMMON, issues #115/
|
||||
# #116/#117/#120) report no oneUiVersion and no '_RAC_'/'-RAC-' token at
|
||||
# all -- the board family is spelled 'ARA-WW-' instead. Same resource
|
||||
@@ -212,8 +220,23 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
||||
key = 'airconditioner'
|
||||
# Air purifiers (e.g. ARTIK051_TVTL_18K, issue #56) report no
|
||||
# oneUiVersion either, and carry the '_TVTL_' board-family token.
|
||||
# Room air conditioners on the ARTIK051 board (e.g. ARTIK051_KRAC_18K,
|
||||
# issue #136) report no oneUiVersion and carry a '_KRAC_' token. The '_RAC_'
|
||||
# check above cannot see it -- the 'K' sits between the underscore and 'RAC' --
|
||||
# and the consumer-prefix fallback only covers washers/dryers/dishwashers, so
|
||||
# these units fell back to 'unknown' and exposed nothing but power. Same
|
||||
# ARTIK051 board family as the '_TVTL_' air purifier below.
|
||||
if key is None and '_KRAC_' in (model_num or ''):
|
||||
key = 'airconditioner'
|
||||
if key is None and '_TVTL_' in (model_num or ''):
|
||||
key = 'air_purifier'
|
||||
# BESPOKE Cube Air (e.g. A-VTWW-TP2-21-COMMON, issue #151) reports no
|
||||
# oneUiVersion and carries the hyphenated '-VTWW-' board-family token
|
||||
# (distinct from the underscore-delimited '_TVTL_' ARTIK051 family
|
||||
# above). Its fan lives on /wind/strength/vs/0 rather than /mode/vs/0 --
|
||||
# see capabilities/air_purifier.py's WIND_STRENGTH_FAN.
|
||||
if key is None and '-VTWW-' in (model_num or '').upper():
|
||||
key = 'air_purifier'
|
||||
model_identity = f'{model_num} {description}'.upper()
|
||||
if key is None and ('_COOKTOP' in model_identity or '_GB_CT_' in model_identity):
|
||||
key = 'cooktop'
|
||||
@@ -262,6 +285,14 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
||||
# station state) shares no hrefs with anything else already modeled.
|
||||
if key is None and '-VSKR-' in (model_num or '').upper():
|
||||
key = 'vacuum_station'
|
||||
# AirDresser (e.g. DA_DF_A51_20_COMMON, issue #162) -- reports no
|
||||
# oneUiVersion and carries the '_DF_' (Dresser Function) board-family
|
||||
# token. Every resource it exposes (course select, wrinkle-prevent
|
||||
# setting, diagnosis) is already handled by the shared laundry
|
||||
# machinery; it needs its own device type only because none of the
|
||||
# washer/dryer/dishwasher consumer-model prefixes below match it.
|
||||
if key is None and '_DF_' in (model_num or ''):
|
||||
key = 'air_dresser'
|
||||
# Consumer-model prefix from `description` (washer/dryer/dishwasher) --
|
||||
# last, since it's the fuzziest match (a bare 2-letter prefix) and would
|
||||
# otherwise shadow the more specific board-family tokens above.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""AirDresser device registry (issue #162).
|
||||
|
||||
Reuses washer/dryer/dishwasher's shared laundry surface -- job-beginning
|
||||
status, operational state, and diagnosis. air_dresser.py holds the two
|
||||
pieces specific to this device type: a minimal wrinkle-prevent-only
|
||||
/washer/vs/0 capability, and the course select's own translation key.
|
||||
"""
|
||||
from ..capabilities import air_dresser, common, dishwasher, ignored, laundry, operational
|
||||
from ._base import DeviceRegistry, _build
|
||||
|
||||
REGISTRY = DeviceRegistry(
|
||||
name='air_dresser',
|
||||
capabilities=_build([
|
||||
*ignored.IGNORED,
|
||||
*common.UNIVERSAL,
|
||||
*common.POWER,
|
||||
air_dresser.AIR_DRESSER_SETTINGS,
|
||||
air_dresser.AIR_DRESSER_COURSE,
|
||||
air_dresser.AIR_DRESSER_SANITIZE,
|
||||
laundry.JOB_BEGINNING_STATUS,
|
||||
operational.OPERATIONAL_STATE,
|
||||
dishwasher.DIAGNOSIS,
|
||||
]),
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Air-purifier device registry.
|
||||
|
||||
Spans two board generations sharing this one registry (see
|
||||
Spans three board generations sharing this one registry (see
|
||||
capabilities/air_purifier.py's module docstring for the per-href
|
||||
match_fn discriminators that keep them from colliding):
|
||||
|
||||
@@ -12,6 +12,10 @@ match_fn discriminators that keep them from colliding):
|
||||
reported; reuses airconditioner.DISPLAY_LIGHT and airconditioner.MUTE_ONCE
|
||||
for /light/vs/0 and /option/muteonce/vs/0, which are identical shapes on
|
||||
the shared DA-AC- board family.
|
||||
- A-VTWW-TP2-21-COMMON-class (issue #151). Reports no oneUiVersion and no
|
||||
existing modelNum token; falls back to unknown until routed here. Its fan
|
||||
is WIND_STRENGTH_FAN on /wind/strength/vs/0 rather than FAN on
|
||||
/mode/vs/0 -- see that capability's comment.
|
||||
|
||||
Reuses dishwasher.DIAGNOSIS for /diagnosis/vs/0 (identical field/write
|
||||
contract).
|
||||
@@ -33,6 +37,7 @@ REGISTRY = DeviceRegistry(
|
||||
air_purifier.AIRFLOW_VS_FALLBACK,
|
||||
air_purifier.MODE,
|
||||
air_purifier.FAN,
|
||||
air_purifier.WIND_STRENGTH_FAN,
|
||||
air_purifier.DISPLAY,
|
||||
air_purifier.HEPA_FILTER,
|
||||
air_purifier.PANEL_STATUS,
|
||||
|
||||
@@ -22,6 +22,7 @@ REGISTRY = DeviceRegistry(
|
||||
airconditioner.AIR_PURIFY,
|
||||
airconditioner.AUTO_CLEAN,
|
||||
airconditioner.AIR_FILTER,
|
||||
airconditioner.AIR_QUALITY,
|
||||
airconditioner.DISPLAY_LIGHT,
|
||||
airconditioner.MUTE_ONCE,
|
||||
airconditioner.CURRENT_LIMIT,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Capabilities specific to the AirDresser family (Samsung DA_DF-class,
|
||||
issues #162/#157).
|
||||
|
||||
This board family reports no oneUiVersion and no /information/vs/0 token
|
||||
any existing family routes on, so it gets its own device type -- but most
|
||||
of the resources it exposes are already handled by the shared laundry
|
||||
machinery:
|
||||
|
||||
/washer/vs/0 -> AIR_DRESSER_SETTINGS (wrinkle_prevent only -- a
|
||||
dedicated capability rather than reusing
|
||||
dryer.DRYER_SETTINGS wholesale: this board never
|
||||
populates dryLevel/dryTime/dryerType at all, and
|
||||
those are permanently-empty-not-just-absent
|
||||
dryer-only fields on an AirDresser, not merely unset
|
||||
ones, so binding them here would ship three sensors
|
||||
that can never read anything on this device type)
|
||||
/course/vs/0 -> AIR_DRESSER_COURSE (cycle select), table id from
|
||||
/st/airdressercourse/vs/0 (issue #157's
|
||||
DA_DF_TP2_20_COMMON reports "Table_00"; issue #162's
|
||||
DA_DF_A51_20_COMMON has no table resource at all and
|
||||
falls back to the name-only 'cycle' key, same as an
|
||||
unrecognized table on washer/dryer). Options come
|
||||
from laundry.cycle_options -- #157's board populates
|
||||
/wm/editcourse/vs/0's editCourseList directly; #162's
|
||||
has no editcourse resource at all and falls through
|
||||
to cycle_options' supportedOptions decode instead.
|
||||
Course names aren't identified yet for either table
|
||||
(no code->name mapping was reported), so they render
|
||||
as their raw codes until named in translations, same
|
||||
as dryer.py's unidentified codes.
|
||||
/diagnosis/vs/0 -> reuses dishwasher.DIAGNOSIS
|
||||
/airdresseroption/sanitize/vs/0 -> AIR_DRESSER_SANITIZE (issue #157 only;
|
||||
#162's board doesn't report this resource at all)
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import SwitchDesc
|
||||
from .laundry import cycle_select
|
||||
|
||||
|
||||
def _wrinkle_write(p, rep, href=None):
|
||||
if p not in ('On', 'Off'):
|
||||
return None
|
||||
return ['washer', 'vs', '0'], {'x.com.samsung.da.wrinklePrevent': p}
|
||||
|
||||
|
||||
def _sanitize_write(p, rep, href=None):
|
||||
if p not in ('On', 'Off'):
|
||||
return None
|
||||
return ['airdresseroption', 'sanitize', 'vs', '0'], {'x.com.samsung.da.sanitize': p}
|
||||
|
||||
|
||||
AIR_DRESSER_SETTINGS = Capability(
|
||||
href='/washer/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SwitchDesc(key='wrinkle_prevent', field='x.com.samsung.da.wrinklePrevent',
|
||||
icon='mdi:iron',
|
||||
value_fn=lambda v: v == 'On',
|
||||
write_fn=_wrinkle_write),
|
||||
),
|
||||
)
|
||||
|
||||
AIR_DRESSER_COURSE = Capability(
|
||||
href='/course/vs/0',
|
||||
entities=(
|
||||
cycle_select(translation_key='air_dresser_cycle', icon='mdi:tshirt-crew',
|
||||
table_href='/st/airdressercourse/vs/0'),
|
||||
),
|
||||
)
|
||||
|
||||
AIR_DRESSER_SANITIZE = Capability(
|
||||
href='/airdresseroption/sanitize/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SwitchDesc(key='sanitize', field='x.com.samsung.da.sanitize',
|
||||
icon='mdi:weather-sunny',
|
||||
value_fn=lambda v: v == 'On',
|
||||
write_fn=_sanitize_write),
|
||||
),
|
||||
)
|
||||
@@ -74,6 +74,7 @@ from .laundry import bool_option_exists, bool_option_value, option_value, option
|
||||
# are mutually exclusive via this presence check rather than colliding.
|
||||
HREF_MODE = '/mode/vs/0'
|
||||
HREF_AIRFLOW = '/airflow/0'
|
||||
HREF_WIND_STRENGTH = '/wind/strength/vs/0'
|
||||
|
||||
|
||||
def _has_top_level_modes(rep, resources):
|
||||
@@ -136,14 +137,22 @@ DEVICE_ACTIVE = Capability(
|
||||
),
|
||||
)
|
||||
|
||||
def _power_write(power_href, value):
|
||||
"""Shared 'power' payload handling for this family's three FanDesc write
|
||||
functions -- targets whichever power href fan.py's _power_payload picked
|
||||
(the board may only report /power/0); a hardcoded vendor href here would
|
||||
silently no-op on such a board even though the entity's own is_on
|
||||
already falls back to reading it correctly."""
|
||||
if power_href == '/power/0':
|
||||
return ['power', '0'], {'value': bool(value)}
|
||||
return (['power', 'vs', '0'],
|
||||
{'x.com.samsung.da.power': 'On' if value else 'Off'})
|
||||
|
||||
|
||||
def _airflow_fan_write(payload, rep, href=None):
|
||||
kind, value, *args = payload
|
||||
if kind == 'power':
|
||||
power_href = args[0] if args else '/power/vs/0'
|
||||
if power_href == '/power/0':
|
||||
return ['power', '0'], {'value': bool(value)}
|
||||
return (['power', 'vs', '0'],
|
||||
{'x.com.samsung.da.power': 'On' if value else 'Off'})
|
||||
return _power_write(args[0] if args else '/power/vs/0', value)
|
||||
if kind == 'speed':
|
||||
return ['airflow', '0'], {'speed': int(value)}
|
||||
return None
|
||||
@@ -233,15 +242,7 @@ MODE = Capability(
|
||||
def _fan_write(payload, rep, href=None):
|
||||
kind, value, *args = payload
|
||||
if kind == 'power':
|
||||
# Targets whichever power href fan.py's _power_payload picked (the
|
||||
# board may only report /power/0) -- a hardcoded vendor href here
|
||||
# would silently no-op on such a board even though the entity's
|
||||
# own is_on already falls back to reading it correctly.
|
||||
power_href = args[0] if args else '/power/vs/0'
|
||||
if power_href == '/power/0':
|
||||
return ['power', '0'], {'value': bool(value)}
|
||||
return (['power', 'vs', '0'],
|
||||
{'x.com.samsung.da.power': 'On' if value else 'Off'})
|
||||
return _power_write(args[0] if args else '/power/vs/0', value)
|
||||
if kind == 'mode':
|
||||
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': [value]}
|
||||
return None
|
||||
@@ -272,6 +273,38 @@ FAN = Capability(
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _wind_strength_fan_write(payload, rep, href=None):
|
||||
kind, value, *args = payload
|
||||
if kind == 'power':
|
||||
return _power_write(args[0] if args else '/power/vs/0', value)
|
||||
if kind == 'mode':
|
||||
return ['wind', 'strength', 'vs', '0'], {'x.com.samsung.da.modes': value}
|
||||
return None
|
||||
|
||||
|
||||
# A-VTWW-TP2-21-COMMON (issue #151): named preset modes like FAN above, but
|
||||
# on a distinct href with numeric codes ("87"/"89"/"90"/"91") instead of
|
||||
# self-describing supportedModes -- x.com.samsung.da.modesName gives the
|
||||
# actual names (SMART/MAX/WINDFREE/Sleep), read live by fan.py's
|
||||
# LocalThingsAirPurifierFan._label_for_code rather than a hardcoded
|
||||
# per-model map. modes here is a bare string ('87'), not a single-element
|
||||
# list like HREF_MODE's -- _wind_strength_fan_write writes it back as-is.
|
||||
#
|
||||
# key is 'wind_strength_fan', NOT 'fan' -- FAN above shares this registry
|
||||
# and also uses a FanDesc; BoundEntity's unique_id is built from key alone
|
||||
# (entity.py's _key), not href, so two same-key FanDescs in one registry
|
||||
# would collide if a board ever bound both (see AIRFLOW_GENERIC's own
|
||||
# comment on this exact hazard -- missed here in the initial cut).
|
||||
WIND_STRENGTH_FAN = Capability(
|
||||
href=HREF_WIND_STRENGTH,
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
FanDesc(key='wind_strength_fan', translation_key='air_purifier_fan',
|
||||
field='x.com.samsung.da.modes', write_fn=_wind_strength_fan_write),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TP1X_DA-AC-AIR-class additions (issue #130). This board reports several
|
||||
# resources the older ARTIK051_TVTL family never did.
|
||||
@@ -425,4 +458,8 @@ COVERAGE = [
|
||||
# functionState both 'false'). Same "needs a multi-field schedule
|
||||
# editor" treatment as fridge.py's /defrost/reservation/vs/0.
|
||||
Capability(href='/dnd/autosleep/vs/0'),
|
||||
# Empty ({}) on the A-VTWW-TP2-21 dump (issue #151) -- this board's
|
||||
# convenient-mode-equivalent behavior lives entirely in WIND_STRENGTH_FAN
|
||||
# above instead.
|
||||
Capability(href='/mode/convenient/vs/0'),
|
||||
]
|
||||
|
||||
@@ -15,10 +15,142 @@ different schema (see capabilities/__init__.py). They live only in the AC
|
||||
by_type registry.
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import BinarySensorDesc, ClimateDesc, SensorDesc, SwitchDesc
|
||||
from ..entities import (
|
||||
BinarySensorDesc, ClimateDesc, NumberDesc, SelectDesc, SensorDesc, SwitchDesc,
|
||||
)
|
||||
from .common import filter_usage_percent, normalize_temp_unit
|
||||
from .laundry import option_write
|
||||
|
||||
|
||||
def _int(v):
|
||||
try:
|
||||
return int(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _beep_on(rep):
|
||||
"""Beep on/off from the `Volume_*` option token: Volume_Mute = off,
|
||||
Volume_100 (and any non-Mute) = on. None when no Volume_ slot.
|
||||
|
||||
_option_token (defined further below, alongside the legacy-board token
|
||||
helpers) returns the token's *value* half (e.g. 'Mute', '100'), not the
|
||||
full 'Volume_100' token -- shared with _option_token_num/_option_token_on,
|
||||
which this module's other options[] readers already rely on.
|
||||
"""
|
||||
tok = _option_token(rep, 'Volume')
|
||||
if tok is None:
|
||||
return None
|
||||
return tok != 'Mute'
|
||||
|
||||
|
||||
def _beep_write(payload, rep, href=None):
|
||||
"""Toggle beep via a single-token /mode/vs/0 options write (option_write's
|
||||
one-token merge -- a full options RMW reverts on ARTIK051_PRAC). 'On'
|
||||
restores the last non-Mute level rather than forcing Volume_100, so a
|
||||
user's intermediate setting (e.g. Volume_50 set via the cloud) survives an
|
||||
off/on cycle; falls back to 100 when no prior level is known."""
|
||||
if payload not in ('On', 'Off'):
|
||||
return None
|
||||
if payload == 'Off':
|
||||
token = 'Mute'
|
||||
else:
|
||||
prev = _option_token(rep, 'Volume')
|
||||
token = prev if (prev and prev != 'Mute') else '100'
|
||||
return ['mode', 'vs', '0'], {
|
||||
'x.com.samsung.da.options': option_write('Volume', token),
|
||||
}
|
||||
|
||||
|
||||
def _tropical_night_value(rep):
|
||||
"""Tropical night mode level (0-16) from the `Sleep_<N>` option token.
|
||||
|
||||
_option_token returns the token's value half already (e.g. '16' for
|
||||
'Sleep_16'), same convention as _beep_on above.
|
||||
"""
|
||||
tok = _option_token(rep, 'Sleep')
|
||||
if tok is None:
|
||||
return None
|
||||
return _int(tok)
|
||||
|
||||
|
||||
def _tropical_night_write(value, rep, href=None):
|
||||
"""Set tropical night level via a single-token `Sleep_<N>` options write.
|
||||
Samsung cloud counterpart: custom.airConditionerTropicalNightMode (0-16)."""
|
||||
try:
|
||||
level = int(round(float(value)))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if not 0 <= level <= 16:
|
||||
return None
|
||||
return ['mode', 'vs', '0'], {
|
||||
'x.com.samsung.da.options': option_write('Sleep', str(level)),
|
||||
}
|
||||
|
||||
|
||||
def _filter_unit(rep):
|
||||
"""Unit of the filter-usage fields, normalised from filterCapacityUnit
|
||||
('Hour' -> 'h'). Wired through unit_fn so a board advertising a different
|
||||
unit doesn't silently mislabel a duration statistic."""
|
||||
u = rep.get('x.com.samsung.da.filterCapacityUnit')
|
||||
return {'Hour': 'h', 'Minute': 'min', 'Second': 's'}.get(u, u or 'h')
|
||||
|
||||
|
||||
def _threshold_write(payload, rep, href=None):
|
||||
"""filterDesiredUsage is locally writable: a plain scalar POST of the
|
||||
field to /filter/airdustfilter/vs/0 is 2.04-accepted and persists
|
||||
(confirmed live on ARTIK051_PRAC: POST 700 -> 2.04, read-back 700). The
|
||||
Select only surfaces where the device advertises
|
||||
supportedFilterDesiredUsage, so the valid options are known rather than
|
||||
guessed; boards without that enum leave this writable field unexposed."""
|
||||
return ['filter', 'airdustfilter', 'vs', '0'], {
|
||||
'x.com.samsung.da.filterDesiredUsage': payload,
|
||||
}
|
||||
|
||||
|
||||
def _sensor_item_value(items, type_):
|
||||
"""First value of the /sensors/vs/0 item with the given
|
||||
x.com.samsung.da.type. The resource exposes no unit, so no device_class is
|
||||
set until a populated reading + unit is observed (the 'don't guess' rule).
|
||||
|
||||
Dust/FineDust/SuperFineDust report a 2-element array (['0','0']) while
|
||||
CleanLevel/Odor report a single element -- the second element's meaning is
|
||||
unconfirmed, so v[0] is taken as the reading and v[1] is dropped; left as
|
||||
a string rather than coerced numeric because only CleanLevel has
|
||||
corroborating evidence (a top-level x.com.samsung.da.cleanLevel scalar)."""
|
||||
for it in (items or []):
|
||||
if isinstance(it, dict) and it.get('x.com.samsung.da.type') == type_:
|
||||
v = it.get('x.com.samsung.da.value')
|
||||
if isinstance(v, list) and v:
|
||||
return str(v[0])
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _has_sensor_type(type_):
|
||||
"""True when the /sensors/vs/0 items[] array lists an item of this type.
|
||||
|
||||
This proves the type is *listed*, not that the reading is real: issue
|
||||
#166 (ARxxTXFCAWKNEU, board ARTIK051_PRAC_20K) lists all five item types
|
||||
with permanent zero values on both its units, and the reporter confirmed
|
||||
none of these sensors are physically present. A top-level
|
||||
x.com.samsung.da.cleanLevel scalar (separate from the CleanLevel item)
|
||||
looked like a corroborating "this reading is real" signal at first --
|
||||
present alongside genuinely populated readings on tp1x_da_ac_rac_01011
|
||||
and the tp1x_da_ac_air air purifier fixture, absent on every all-zero
|
||||
ARTIK051_PRAC_20K dump including both #166 units -- but it doesn't hold
|
||||
up as a general rule: air_purifier_device.json (ARTIK051_TVTL_18K),
|
||||
air_purifier_vtww_device.json, and range_hood_device.json all carry
|
||||
genuinely populated, non-AC-family Dust/FineDust/SuperFineDust readings
|
||||
with no such scalar. So this stays item-type presence only -- the
|
||||
entities are disabled by default instead (see AIR_QUALITY below) rather
|
||||
than existence-gated on a signal that would silently drop real readings
|
||||
on hardware this repo hasn't seen yet."""
|
||||
def fn(rep, resources):
|
||||
return any(isinstance(i, dict) and i.get('x.com.samsung.da.type') == type_
|
||||
for i in (rep.get('x.com.samsung.da.items') or []))
|
||||
return fn
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Canonical AC resource hrefs. The climate entity (climate.py) binds the
|
||||
# primary HREF_MODE via CLIMATE below and reads the CLIMATE_CONSUMED_HREFS
|
||||
@@ -43,11 +175,18 @@ HREF_WIND_DIRECTION = '/wind/direction/vs/0' # swing_mode
|
||||
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)
|
||||
# Legacy ARTIK051 boards (ARTIK051_KRAC_18K, issue #136) carry no /wind/* resources
|
||||
# at all: fan speed and vane direction sit together in this one resource, as
|
||||
# x.com.samsung.da.speedLevel (the same 0-4 scale as _DEVICE_TO_FAN) and
|
||||
# x.com.samsung.da.direction (the same codes as _DEVICE_TO_SWING). Their
|
||||
# convenient-mode preset is a Comode_* token in /mode/vs/0's options instead of a
|
||||
# resource of its own -- see climate.py's _legacy_airflow/_legacy_convenient.
|
||||
HREF_AIRFLOW = '/airflow/vs/0' # legacy fan_mode + swing_mode
|
||||
|
||||
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_WIND_OSCILLATION, HREF_CONVENIENT,
|
||||
HREF_WIND_OSCILLATION, HREF_CONVENIENT, HREF_AIRFLOW,
|
||||
]
|
||||
|
||||
|
||||
@@ -127,6 +266,96 @@ def _display_light_write(payload, rep, href=None):
|
||||
{'x.com.samsung.da.options': option_write('Light', token)})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Legacy ARTIK051 boards keep several settings that newer boards expose as their
|
||||
# own resources (/option/*, /electriccurrent/vs/0, ...) as `<Prefix>_<value>`
|
||||
# tokens inside /mode/vs/0's options blob instead. Reads pull the token apart;
|
||||
# writes reuse option_write's single-token merge, exactly like the display light.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _option_token(rep, prefix):
|
||||
"""Value part of a `<prefix>_<value>` token in /mode/vs/0's options."""
|
||||
for option in _mode_options(rep):
|
||||
if isinstance(option, str) and option.startswith(prefix + '_'):
|
||||
return option.split('_', 1)[1]
|
||||
return None
|
||||
|
||||
|
||||
def is_legacy_board(resources):
|
||||
"""True for the board generation whose airflow lives in /airflow/vs/0.
|
||||
|
||||
Newer families carry several of the same option tokens (Volume, Sleep,
|
||||
OutdoorTemp, Autoclean) *alongside* dedicated resources for those settings,
|
||||
so an ungated token entity would either duplicate an existing one or apply a
|
||||
scale calibrated elsewhere. Every AC dump on record has one shape or the
|
||||
other: /airflow/vs/0 with no /wind/* at all, or /wind/strength/vs/0 with no
|
||||
/airflow/vs/0. Same test as climate.py's _legacy_airflow(), so the entities
|
||||
below and the climate entity can never disagree about the generation.
|
||||
"""
|
||||
return HREF_AIRFLOW in resources and HREF_WIND_STRENGTH not in resources
|
||||
|
||||
|
||||
def _has_option_token(prefix):
|
||||
return lambda rep, resources: (
|
||||
is_legacy_board(resources) and _option_token(rep, prefix) is not None)
|
||||
|
||||
|
||||
def _option_token_on(prefix):
|
||||
return lambda rep: _option_token(rep, prefix) == 'On'
|
||||
|
||||
|
||||
def _option_token_num(prefix, offset=0, divisor=1):
|
||||
def read(rep):
|
||||
raw = _option_token(rep, prefix)
|
||||
try:
|
||||
return (float(raw) - offset) / divisor
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return read
|
||||
|
||||
|
||||
def _option_switch_write(prefix):
|
||||
def write(payload, rep, href=None):
|
||||
return (['mode', 'vs', '0'],
|
||||
{'x.com.samsung.da.options': option_write(prefix, payload)})
|
||||
return write
|
||||
|
||||
|
||||
def _option_number_write(prefix):
|
||||
def write(payload, rep, href=None):
|
||||
return (['mode', 'vs', '0'],
|
||||
{'x.com.samsung.da.options':
|
||||
option_write(prefix, str(int(round(float(payload)))))})
|
||||
return write
|
||||
|
||||
|
||||
def _humidity(rep):
|
||||
"""Relative humidity, preferring the 5%-rounded field where it exists.
|
||||
|
||||
ARTIK051 boards have no fivepercentHumidity field at all and report the
|
||||
plain x.com.samsung.da.humidity instead -- and only populate it while the
|
||||
Air monitoring option is on: the unit measures for roughly half a minute
|
||||
(51% observed, matching what the same unit's cloud integration reported at
|
||||
that moment), then zeroes the field and switches Air monitoring back off by
|
||||
itself. So 0 reads as "not measuring" and is reported as unknown rather than
|
||||
as 0% humidity, which would poison long-term history.
|
||||
|
||||
That zero-as-"not measuring" carve-out is specific to the ARTIK051
|
||||
fallback field's hardware quirk -- every other board's fivepercentHumidity
|
||||
has never been documented getting stuck at zero, and collapsing a
|
||||
genuine 0% reading there to unknown is a regression, not a safeguard
|
||||
(issue #160). So fivepercentHumidity passes 0 through unchanged; only the
|
||||
humidity fallback applies the zero-collapse.
|
||||
"""
|
||||
if 'x.com.samsung.da.fivepercentHumidity' in rep:
|
||||
return _num(rep['x.com.samsung.da.fivepercentHumidity'])
|
||||
if 'x.com.samsung.da.humidity' in rep:
|
||||
value = _num(rep['x.com.samsung.da.humidity'])
|
||||
return value if value else None
|
||||
return None
|
||||
|
||||
|
||||
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
|
||||
@@ -180,6 +409,16 @@ def _climate_write(payload, rep, href=None):
|
||||
'vertical': 'Swing' if value in ('vertical', 'both') else 'Fix',
|
||||
'horizontal': 'Swing' if value in ('horizontal', 'both') else 'Fix',
|
||||
})
|
||||
if kind == 'fan_legacy':
|
||||
return (['airflow', 'vs', '0'],
|
||||
{'x.com.samsung.da.speedLevel': str(value)})
|
||||
if kind == 'swing_legacy':
|
||||
return (['airflow', 'vs', '0'],
|
||||
{'x.com.samsung.da.direction': value})
|
||||
if kind == 'preset_legacy':
|
||||
# Single-token options merge, same mechanism as _display_light_write.
|
||||
return (['mode', 'vs', '0'],
|
||||
{'x.com.samsung.da.options': option_write('Comode', value)})
|
||||
if kind == 'preset':
|
||||
return (['mode', 'convenient', 'vs', '0'], {'x.com.samsung.da.modes': value})
|
||||
return None
|
||||
@@ -204,6 +443,95 @@ CLIMATE = Capability(
|
||||
exists_fn=_has_display_light_option,
|
||||
write_fn=_display_light_write,
|
||||
icon='mdi:led-on', entity_category='config'),
|
||||
# Beep on/off from the `Volume_*` option token (Volume_Mute/Volume_100).
|
||||
# Single-token option_write; a full options RMW reverts on ARTIK051_PRAC.
|
||||
# Gated off the legacy ARTIK051 board generation (see is_legacy_board):
|
||||
# that generation's own Volume_ token is already modeled as the
|
||||
# buzzer_volume Number below, and both reading the same options[] slot
|
||||
# into two entities would be redundant.
|
||||
SwitchDesc(key='beep', rep_fn=_beep_on,
|
||||
exists_fn=lambda rep, resources: (
|
||||
not is_legacy_board(resources)
|
||||
and _option_token(rep, 'Volume') is not None),
|
||||
write_fn=_beep_write,
|
||||
icon='mdi:volume-high', entity_category='config'),
|
||||
# Tropical night mode level (0-16) from the `Sleep_<N>` option token.
|
||||
# Single-token option_write. Cloud: custom.airConditionerTropicalNightMode.
|
||||
# Gated off the legacy board for the same reason as beep above -- its
|
||||
# Sleep_ token is already the good_sleep Number below.
|
||||
#
|
||||
# exists_fn only proves the Sleep_ token slot is present, not that
|
||||
# tropical night mode is a real feature of the unit: issue #166
|
||||
# (ARxxTXFCAWKNEU) reports Sleep_0 in every dump -- the exact same
|
||||
# always-there-at-zero shape as the issue #17 dump #164 was verified
|
||||
# against -- yet the reporter confirmed their remote/app has no
|
||||
# tropical night mode control at all. Samsung's OCF options[] blob
|
||||
# carries this scaffolding token regardless of physical capability,
|
||||
# so there's no reliable signal here to gate on (same 'don't guess'
|
||||
# rule as elsewhere in this file, just with no signal to guess from).
|
||||
# Registered but disabled by default, same precedent as
|
||||
# fridge.rack_count / cooktop.paired_hood_* -- units that do have the
|
||||
# feature can enable it themselves.
|
||||
NumberDesc(key='tropical_night_mode', rep_fn=_tropical_night_value,
|
||||
exists_fn=lambda rep, resources: (
|
||||
not is_legacy_board(resources)
|
||||
and _option_token(rep, 'Sleep') is not None),
|
||||
write_fn=_tropical_night_write,
|
||||
native_min=0, native_max=16, step=1,
|
||||
enabled_default=False,
|
||||
icon='mdi:weather-night', entity_category='config'),
|
||||
# Settings that this board generation keeps as options[] tokens.
|
||||
SwitchDesc(key='spi', rep_fn=_option_token_on('Spi'),
|
||||
exists_fn=_has_option_token('Spi'),
|
||||
write_fn=_option_switch_write('Spi'),
|
||||
icon='mdi:air-purifier', entity_category='config'),
|
||||
# Shares AUTO_CLEAN's catalog entry rather than duplicating it: same
|
||||
# feature, different board generation (that one is a /option/autoclean/
|
||||
# vs/0 field, absent here). Distinct key, so nothing collides if some
|
||||
# future board ever reported both.
|
||||
SwitchDesc(key='auto_clean_legacy', translation_key='auto_clean',
|
||||
rep_fn=_option_token_on('Autoclean'),
|
||||
exists_fn=_has_option_token('Autoclean'),
|
||||
write_fn=_option_switch_write('Autoclean'),
|
||||
icon='mdi:fan-auto', entity_category='config'),
|
||||
SwitchDesc(key='air_monitoring', rep_fn=_option_token_on('AirMonitoring'),
|
||||
exists_fn=_has_option_token('AirMonitoring'),
|
||||
write_fn=_option_switch_write('AirMonitoring'),
|
||||
icon='mdi:air-filter', entity_category='config'),
|
||||
NumberDesc(key='buzzer_volume', rep_fn=_option_token_num('Volume'),
|
||||
exists_fn=_has_option_token('Volume'),
|
||||
write_fn=_option_number_write('Volume'),
|
||||
native_min=0, native_max=100, step=10,
|
||||
icon='mdi:volume-high', entity_category='config'),
|
||||
# "Good Sleep" timer. 0 = off; the upper bound is a guess (the token
|
||||
# carries no range hint and only 0 has been observed on hardware), so a
|
||||
# write above 0 is unverified.
|
||||
NumberDesc(key='good_sleep', rep_fn=_option_token_num('Sleep'),
|
||||
exists_fn=_has_option_token('Sleep'),
|
||||
write_fn=_option_number_write('Sleep'),
|
||||
native_min=0, native_max=12, step=1, unit='h',
|
||||
icon='mdi:sleep', entity_category='config'),
|
||||
# Outdoor temperature, offset by 55. Two calibration points on one unit:
|
||||
# token 75 while an independent outdoor thermometer in the same install
|
||||
# read 20.3 C, and token 74 against a 19.4 C forecast. Fahrenheit fits far
|
||||
# worse (74 F = 23.3 C); the issue #136 unit's 81 gives 26 C in a warmer
|
||||
# climate, which is also plausible.
|
||||
SensorDesc(key='outdoor_temperature',
|
||||
rep_fn=_option_token_num('OutdoorTemp', offset=55),
|
||||
exists_fn=_has_option_token('OutdoorTemp'),
|
||||
device_class='temperature', state_class='measurement',
|
||||
unit='°C', icon='mdi:home-thermometer-outline'),
|
||||
# Filter time in tenths of an hour: the token read 1710 while the official
|
||||
# Samsung app displayed "171 hours 0 minutes" for the filter on the same
|
||||
# unit, and the .0 matching the app's "0 minutes" pins the scale. Whether
|
||||
# it counts up or down is NOT established -- it was seen rising (171.0 ->
|
||||
# 171.5) while the unit ran, which contradicts the app's "remaining"
|
||||
# wording, so the entity is deliberately named neutrally.
|
||||
SensorDesc(key='filter_time',
|
||||
rep_fn=_option_token_num('FilterTime', divisor=10),
|
||||
exists_fn=_has_option_token('FilterTime'),
|
||||
device_class='duration', unit='h',
|
||||
state_class='measurement', icon='mdi:air-filter'),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -242,6 +570,28 @@ AIR_FILTER = Capability(
|
||||
SensorDesc(key='air_filter_usage', rep_fn=filter_usage_percent,
|
||||
unit='%', state_class='measurement',
|
||||
icon='mdi:air-filter', entity_category='diagnostic'),
|
||||
# filterUsage is a lifetime hour counter that only resets on filter
|
||||
# replacement -- total_increasing so HA's long-term statistics handle
|
||||
# the reset rather than treating it as a bounded measurement.
|
||||
SensorDesc(key='air_filter_usage_hours',
|
||||
field='x.com.samsung.da.filterUsage',
|
||||
device_class='duration',
|
||||
state_class='total_increasing',
|
||||
unit_fn=_filter_unit,
|
||||
icon='mdi:air-filter', entity_category='diagnostic',
|
||||
value_fn=_int),
|
||||
# The alarm threshold (filterDesiredUsage) is a locally writable option:
|
||||
# see _threshold_write. Surfaces as a Select only where the device
|
||||
# advertises supportedFilterDesiredUsage; boards without that enum
|
||||
# leave it unexposed rather than guess the valid set.
|
||||
SelectDesc(key='air_filter_threshold',
|
||||
field='x.com.samsung.da.filterDesiredUsage',
|
||||
options_field='x.com.samsung.da.supportedFilterDesiredUsage',
|
||||
exists_fn=lambda rep, res: bool(
|
||||
rep.get('x.com.samsung.da.supportedFilterDesiredUsage')),
|
||||
icon='mdi:alarm', entity_category='config',
|
||||
write_fn=_threshold_write,
|
||||
value_fn=lambda v: str(v) if v is not None else None),
|
||||
SensorDesc(key='air_filter_status', field='x.com.samsung.da.filterStatus',
|
||||
device_class='enum',
|
||||
options=('normal', 'wash', 'replace'),
|
||||
@@ -357,18 +707,73 @@ CURRENT_TEMPERATURE_VS = Capability(
|
||||
# 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).
|
||||
# stuck "0" there, so /humidity/0 stays ignored per the 'don't guess' rule
|
||||
# (see _AC_IGNORED below).
|
||||
#
|
||||
# ARTIK051 boards (issue #136) have no fivepercentHumidity field at all, and
|
||||
# there the plain `humidity` field is not stuck: it carries a real reading
|
||||
# (51%, matching the same unit's cloud integration at that moment) for as long
|
||||
# as the Air monitoring option is on, which the unit itself switches back off
|
||||
# after roughly a minute -- so most dumps catch it at 0. Hence _humidity's
|
||||
# fallback, and hence 0 reading as "not measuring" rather than 0%: on both
|
||||
# board generations a zero here means no measurement, never dry air.
|
||||
# /humidity/0 stayed 0 throughout that same observation too.
|
||||
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),
|
||||
SensorDesc(key='humidity', rep_fn=_humidity,
|
||||
device_class='humidity', state_class='measurement', unit='%'),
|
||||
),
|
||||
)
|
||||
|
||||
# /sensors/vs/0 items[] carry live air-quality readings. Removed from
|
||||
# _AC_IGNORED below so AIR_QUALITY is the sole cap on the href. CleanLevel is
|
||||
# corroborated as numeric by a top-level x.com.samsung.da.cleanLevel scalar
|
||||
# (tp1x_da_ac_rac_01011 reports both as '1'), so it's a measurement; the others
|
||||
# are 1- or 2-element arrays with no corroborating scalar, so they stay string
|
||||
# diagnostics (see _sensor_item_value for the 2-element ambiguity and why only
|
||||
# v[0] is taken). No unit is advertised on the resource, so no device_class.
|
||||
#
|
||||
# exists_fn (_has_sensor_type) only proves the item *type* is listed, not
|
||||
# that the unit actually carries that sensor: issue #166 (ARxxTXFCAWKNEU,
|
||||
# board ARTIK051_PRAC_20K) reports all five item types on both its units,
|
||||
# values permanently '0'/['0','0'] -- yet the reporter confirmed none apply
|
||||
# to their model. A tighter existence gate was tried (requiring the
|
||||
# corroborating cleanLevel scalar above) but doesn't hold up as a general
|
||||
# rule -- see _has_sensor_type's docstring -- and risks silently dropping
|
||||
# real readings on hardware that reports them without that scalar. So these
|
||||
# stay bound whenever the type is listed, same as before #166, and disabled
|
||||
# by default instead (same precedent as fridge.rack_count /
|
||||
# cooktop.paired_hood_model / this file's own tropical_night_mode): units
|
||||
# that do have the sensor can enable it themselves.
|
||||
AIR_QUALITY = Capability(
|
||||
href='/sensors/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SensorDesc(key='clean_level', field='x.com.samsung.da.items',
|
||||
icon='mdi:broom', entity_category='diagnostic',
|
||||
state_class='measurement',
|
||||
exists_fn=_has_sensor_type('CleanLevel'),
|
||||
enabled_default=False,
|
||||
value_fn=lambda items: _int(_sensor_item_value(items, 'CleanLevel'))),
|
||||
*tuple(
|
||||
SensorDesc(key=key, field='x.com.samsung.da.items',
|
||||
icon=icon, entity_category='diagnostic',
|
||||
exists_fn=_has_sensor_type(type_),
|
||||
enabled_default=False,
|
||||
value_fn=lambda items, t=type_: _sensor_item_value(items, t))
|
||||
for key, icon, type_ in (
|
||||
('odor', 'mdi:weather-windy', 'Odor'),
|
||||
('dust', 'mdi:cloud', 'Dust'),
|
||||
('fine_dust', 'mdi:cloud-outline', 'FineDust'),
|
||||
('super_fine_dust', 'mdi:weather-fog', 'SuperFineDust'),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AC-scoped coverage: the CLIMATE_CONSUMED_HREFS above (read by the climate
|
||||
# entity) plus vendor duplicates / all-zero-ambiguous / plumbing resources.
|
||||
@@ -388,15 +793,18 @@ HUMIDITY = Capability(
|
||||
# of waiting on the summary sweep.
|
||||
# ---------------------------------------------------------------------------
|
||||
_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',
|
||||
# Presence-personalization plumbing (empty item list here).
|
||||
'/personality/presence/vs/0',
|
||||
# OCF-standard mirror of /airflow/vs/0 ({speed, direction} vs
|
||||
# {speedLevel, direction}, identical values). The climate entity reads and
|
||||
# writes the vendor form, which is the one confirmed on hardware here --
|
||||
# note that air_purifier.py found the opposite ordering on its family, so
|
||||
# neither form is reliable sight-unseen and this one stays unmodeled.
|
||||
'/airflow/0',
|
||||
# --- TP1X/TP2X-class housekeeping / opaque blobs. These carry no
|
||||
# user-actionable state or no documented write contract, so per the
|
||||
# 'don't guess' rule they are ignored rather than modeled.
|
||||
@@ -427,6 +835,19 @@ _AC_IGNORED = [
|
||||
# topology -- indoor/outdoor unit pairing, per-unit serials, MCU info.
|
||||
# Commissioning-time plumbing, not user-actionable appliance state.
|
||||
'/sac/installationinfo/vs/0',
|
||||
# Wind-Free 2-in-1 systems (one outdoor unit driving a floor-standing
|
||||
# *and* a wall-mounted indoor unit over one shared local IP, e.g.
|
||||
# TP2X_FAC_BORA_21K, issues #150/#153): an opaque paired-subdevice id
|
||||
# list, same "remote device ids, not user-actionable locally" role as
|
||||
# /remotedeviceinfo/vs/0 above. This integration talks to whichever
|
||||
# single local endpoint the config entry was set up against; the
|
||||
# second indoor unit isn't independently reachable through this
|
||||
# resource (or any other in the dump) -- it would need its own local
|
||||
# DTLS session/IP, which SmartThings pairing doesn't expose here.
|
||||
'/subdevices/vs/0',
|
||||
# Undocumented single int (runningMode: 0 on every dump seen), no
|
||||
# supported-values list to interpret it against -- 'don't guess'.
|
||||
'/runn/vs/0',
|
||||
]
|
||||
|
||||
# Built as bare no-entity caps; folded into the AC registry (not global).
|
||||
|
||||
@@ -91,12 +91,25 @@ def _ml_to_l(v):
|
||||
|
||||
|
||||
def _active_alarm_codes(items):
|
||||
"""Join active alarm codes; skip retained rows Samsung leaves as Deleted.
|
||||
"""Join active alarm codes; skip retained rows Samsung leaves as Deleted,
|
||||
and any code ending in '_OFF'.
|
||||
|
||||
Laundry boards keep a Deleted ErrorCode row in /alarms/vs/0 after the
|
||||
condition clears (see WD7000B diagnostics). Surface only live alarms so
|
||||
HA doesn't stick on a stale ErrorCode. Range-hood has its own stricter
|
||||
helper that also drops ErrorCode_OFF.
|
||||
HA doesn't stick on a stale ErrorCode.
|
||||
|
||||
Samsung pre-populates this array with one row per alarm *type* the board
|
||||
supports, each carrying its own '<Name>_OFF' placeholder code when that
|
||||
alarm isn't firing -- confirmed across independent device families
|
||||
(ErrorCode_OFF, FilterAlarm_OFF, OV_E_OFF, CT_E_OFF, WaterTankFull_OFF,
|
||||
AC_V_0002_OFF all appear in fixtures with no corresponding active
|
||||
condition). An alarm that's actually firing instead reports a plain,
|
||||
unsuffixed code (FilterAlarm, DoorA_Opened, SNSF_Reached) -- issue #166's
|
||||
AC dump has both a FilterAlarm_OFF placeholder and shows what a live
|
||||
filter alert looks like: code 'FilterAlarm' (no suffix), state
|
||||
'Created'. Range-hood previously special-cased only the literal
|
||||
'ErrorCode_OFF' string in its own stricter helper; this generalizes
|
||||
the same rule to the whole '_OFF' suffix convention.
|
||||
"""
|
||||
if not items or not isinstance(items, list):
|
||||
return 'none'
|
||||
@@ -105,6 +118,7 @@ def _active_alarm_codes(items):
|
||||
for i in items
|
||||
if i.get('x.com.samsung.da.code')
|
||||
and str(i.get('x.com.samsung.da.state', '')).lower() != 'deleted'
|
||||
and not str(i.get('x.com.samsung.da.code', '')).lower().endswith('_off')
|
||||
]
|
||||
return ', '.join(codes) if codes else 'none'
|
||||
|
||||
|
||||
@@ -53,6 +53,10 @@ IGNORED: list[Capability] = [
|
||||
Capability(href='/setting/vs/0'), # supported/selected UI language
|
||||
Capability(href='/timezone/vs/0'), # redundant with HA's own timezone
|
||||
Capability(href='/wm/setinfo/vs/0'), # model/manufacturing metadata
|
||||
# Resource-monitoring poll-interval config (a bare minPeriod in
|
||||
# milliseconds, issue #165's TP1X_REF_21K fridge) -- internal transport
|
||||
# plumbing, not appliance state.
|
||||
Capability(href='/rm/control/vs/0'),
|
||||
|
||||
# Demand Response Load Control — utility-company grid signals; requires
|
||||
# cloud registration with a utility program we don't support locally.
|
||||
@@ -107,6 +111,11 @@ IGNORED: list[Capability] = [
|
||||
# exposed by dryer.DRYER_COURSE at /course/vs/0
|
||||
# (x.com.samsung.da.st.dryerMode is "Table_03_Course_<same hex code>").
|
||||
Capability(href='/st/dryercourse/vs/0'),
|
||||
# AirDresser counterpart of the above (issue #157): read only for its
|
||||
# courseTable id (air_dresser.AIR_DRESSER_COURSE's table_href), no
|
||||
# entity of its own -- same "no entity, just the table id" role as
|
||||
# /st/washercourse/vs/0 and /st/dryercourse/vs/0.
|
||||
Capability(href='/st/airdressercourse/vs/0'),
|
||||
# Empty on every washer dump seen so far.
|
||||
Capability(href='/wm/welcomemsg/vs/0'),
|
||||
# User-saved custom course slots (F1-FA). No controllable/observable
|
||||
|
||||
@@ -21,7 +21,12 @@ different from an oven, and defined fresh here:
|
||||
* Lamp: this family's option-array token is bare 'Lamp' (issue #137's
|
||||
'Lamp_Off'), not oven.py's 'UpperLamp' -- and it's genuinely absent on
|
||||
the combi dump (issue #121), so it's gated with exists_fn rather than
|
||||
assumed universal like oven.py's lamp switch.
|
||||
assumed universal like oven.py's lamp switch. Issue #137's dump only
|
||||
ever showed 'Off', so 'On' was a guess at the paired value; issue #152's
|
||||
ME7500D dump is the first to show a real non-Off value, and it's 'High'
|
||||
(a brightness level, not literally 'On') -- the switch now treats any
|
||||
non-Off/non-None value as "on" for reads, and writes back 'High'/'Off'
|
||||
(the two confirmed tokens) rather than the never-confirmed 'On'.
|
||||
|
||||
Note: cooking-mode writes are unproven here, same caveat as oven.py's
|
||||
OVEN_MODE -- exposed as a SelectDesc for fidelity, first real-world write
|
||||
@@ -107,8 +112,21 @@ def _power_level_watts(v):
|
||||
return int_or_none(s)
|
||||
|
||||
|
||||
def _cooking_mode_options(resources):
|
||||
"""Live mode list from the device's own /mode/vs/0 supportedModes when
|
||||
it reports one (both known dumps do); the union-of-all-dumps
|
||||
_MICROWAVE_MODES guess otherwise. Same live-first, static-fallback
|
||||
pattern as oven._oven_mode_options -- a fixed list here would offer
|
||||
users modes their own unit doesn't have (issue #152's ME7500D reports
|
||||
only 4 of _MICROWAVE_MODES' 11)."""
|
||||
rep = resources.get('/mode/vs/0') or {}
|
||||
live = rep.get('x.com.samsung.da.supportedModes')
|
||||
return list(live) if live else list(_MICROWAVE_MODES)
|
||||
|
||||
|
||||
def _mode_write(p, rep, href=None):
|
||||
if p not in _MICROWAVE_MODES:
|
||||
valid = rep.get('x.com.samsung.da.supportedModes') or _MICROWAVE_MODES
|
||||
if p not in valid:
|
||||
return None
|
||||
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': [p]}
|
||||
|
||||
@@ -132,8 +150,12 @@ def _lamp_write(p, rep, href=None):
|
||||
return None
|
||||
if not rep.get('x.com.samsung.da.options'):
|
||||
return None
|
||||
# 'High' and 'Off' are the two tokens actually confirmed on live dumps
|
||||
# (issues #137/#152) -- 'On' has never been observed and the device
|
||||
# likely doesn't recognize it (see module docstring).
|
||||
token = 'High' if p == 'On' else 'Off'
|
||||
return ['mode', 'vs', '0'], {
|
||||
'x.com.samsung.da.options': option_write('Lamp', p),
|
||||
'x.com.samsung.da.options': option_write('Lamp', token),
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +200,7 @@ MICROWAVE_MODE = Capability(
|
||||
# SelectDesc first — test_microwave_mode_options_nonempty uses entities[0]
|
||||
SelectDesc(key='cooking_mode', field='x.com.samsung.da.modes',
|
||||
icon='mdi:tune',
|
||||
options=_MICROWAVE_MODES,
|
||||
options=_cooking_mode_options,
|
||||
value_fn=lambda v: v[0] if v else None,
|
||||
write_fn=_mode_write),
|
||||
SwitchDesc(key='sound', field='x.com.samsung.da.options',
|
||||
@@ -189,7 +211,7 @@ MICROWAVE_MODE = Capability(
|
||||
SwitchDesc(key='lamp', field='x.com.samsung.da.options',
|
||||
icon='mdi:track-light',
|
||||
exists_fn=_lamp_exists,
|
||||
value_fn=lambda opts: option_value(opts, 'Lamp') == 'On',
|
||||
value_fn=lambda opts: option_value(opts, 'Lamp') not in (None, 'Off'),
|
||||
write_fn=_lamp_write),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -51,6 +51,14 @@ SETPOINT_STEP_F = 5
|
||||
# Mode options seen on NV7000BS-class. No dump exists so this list is inferred
|
||||
# from Samsung documentation and firmware observations. The firmware will
|
||||
# reject unknown modes; missing entries here are a coverage gap, not a bug.
|
||||
#
|
||||
# This is a fallback only, used when a device's own /mode/vs/0 doesn't report
|
||||
# x.com.samsung.da.supportedModes at all -- see _oven_mode_options/
|
||||
# _oven_mode_write below. issue #138's range dump (NE63A6511SS/AA) reports
|
||||
# ConvectionRoast/KeepWarm/BreadProof/AirFryer/Dehydrate/SelfClean/SteamClean
|
||||
# in its own supportedModes; those are read live rather than added here, per
|
||||
# the adding-device-support skill's preference for device-reported option
|
||||
# lists over hardcoded ones.
|
||||
_OVEN_MODES = (
|
||||
'NoOperation',
|
||||
'Bake',
|
||||
@@ -173,8 +181,20 @@ def _cook_time_write(p, rep, href=None):
|
||||
}
|
||||
|
||||
|
||||
def _oven_mode_options(resources):
|
||||
"""Live mode list from the device's own /mode/vs/0 supportedModes when
|
||||
it reports one; the NV7000BS-era _OVEN_MODES guess otherwise. Mirrors
|
||||
laundry.py's options_field pattern (buzzer_sound/finish_sound), but
|
||||
needs the callable form rather than options_field because a static
|
||||
fallback has to kick in when the device's own field is absent."""
|
||||
rep = resources.get('/mode/vs/0') or {}
|
||||
live = rep.get('x.com.samsung.da.supportedModes')
|
||||
return list(live) if live else list(_OVEN_MODES)
|
||||
|
||||
|
||||
def _oven_mode_write(p, rep, href=None):
|
||||
if p not in _OVEN_MODES:
|
||||
valid = rep.get('x.com.samsung.da.supportedModes') or _OVEN_MODES
|
||||
if p not in valid:
|
||||
return None
|
||||
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': [p]}
|
||||
|
||||
@@ -351,7 +371,7 @@ OVEN_MODE = Capability(
|
||||
# SelectDesc first — test_oven_mode_options_nonempty uses entities[0]
|
||||
SelectDesc(key='oven_mode', field='x.com.samsung.da.modes',
|
||||
icon='mdi:tune',
|
||||
options=_OVEN_MODES,
|
||||
options=_oven_mode_options,
|
||||
value_fn=lambda v: v[0] if v else None,
|
||||
write_fn=_oven_mode_write),
|
||||
SwitchDesc(key='lamp', field='x.com.samsung.da.options',
|
||||
|
||||
@@ -73,7 +73,15 @@ def discover(
|
||||
registry: dict[str, list[Capability]],
|
||||
pattern_caps: Iterable[Capability] = (),
|
||||
log: Optional[Callable[[str], None]] = None,
|
||||
tier_log: Optional[Callable[[str, str], None]] = None,
|
||||
) -> list[BoundEntity]:
|
||||
"""`tier_log(href, poll_tier)` fires for every href a capability actually
|
||||
matches, even a no-entity "coverage-only" capability (see COVERAGE lists
|
||||
in capabilities/*.py) that `_bind()` turns into zero `BoundEntity` rows.
|
||||
Callers that need a href's poll cadence (the coordinator's hot/warm
|
||||
sub-poll and OBSERVE-attempt lists) must use this, not `bound` -- a
|
||||
coverage-only capability's `poll_tier` would otherwise be silently
|
||||
dropped since it never appears in `bound`."""
|
||||
out: list[BoundEntity] = []
|
||||
|
||||
for href, rep in resources.items():
|
||||
@@ -91,6 +99,8 @@ def discover(
|
||||
inst = instance_suffix(href)
|
||||
out.extend(_bind(cap, href, inst, _instance_name(cap, rep)))
|
||||
matched = True
|
||||
if tier_log is not None:
|
||||
tier_log(href, cap.poll_tier)
|
||||
|
||||
if matched:
|
||||
continue
|
||||
@@ -109,6 +119,8 @@ def discover(
|
||||
segs = [s for s in src.strip('/').split('/') if s and not s.isdigit() and s != 'vs']
|
||||
out.extend(_bind(cap, href, inst, _instance_name(cap, rep), '_'.join(segs)))
|
||||
matched = True
|
||||
if tier_log is not None:
|
||||
tier_log(href, cap.poll_tier)
|
||||
break
|
||||
|
||||
if not matched and not caps and log is not None:
|
||||
|
||||
@@ -120,7 +120,8 @@
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"turbo": "Turbo"
|
||||
"turbo": "Turbo",
|
||||
"max": "Max"
|
||||
}
|
||||
},
|
||||
"preset_mode": {
|
||||
@@ -134,7 +135,8 @@
|
||||
"longwind": "Long wind",
|
||||
"motionindirect": "Motion indirect",
|
||||
"motiondirect": "Motion direct",
|
||||
"drycomfort": "Dry comfort"
|
||||
"drycomfort": "Dry comfort",
|
||||
"2step": "2-Step"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,6 +158,9 @@
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"buzzer_volume": {
|
||||
"name": "Beep volume"
|
||||
},
|
||||
"cook_time": {
|
||||
"name": "Cook time"
|
||||
},
|
||||
@@ -165,6 +170,9 @@
|
||||
"dispense_capacity": {
|
||||
"name": "Dispense capacity"
|
||||
},
|
||||
"good_sleep": {
|
||||
"name": "Good Sleep"
|
||||
},
|
||||
"instance_setpoint": {
|
||||
"name": "{instance_name} setpoint"
|
||||
},
|
||||
@@ -179,12 +187,18 @@
|
||||
},
|
||||
"target_humidity": {
|
||||
"name": "Target humidity"
|
||||
},
|
||||
"tropical_night_mode": {
|
||||
"name": "Tropical night mode"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"ai_energy_level": {
|
||||
"name": "AI Energy Mode level"
|
||||
},
|
||||
"air_filter_threshold": {
|
||||
"name": "Filter alarm threshold"
|
||||
},
|
||||
"beverage_zone_mode": {
|
||||
"name": "Beverage zone mode",
|
||||
"state": {
|
||||
@@ -572,6 +586,9 @@
|
||||
"air_filter_usage": {
|
||||
"name": "Filter usage"
|
||||
},
|
||||
"air_filter_usage_hours": {
|
||||
"name": "Filter usage hours"
|
||||
},
|
||||
"air_sensing_state": {
|
||||
"name": "Air sensing state"
|
||||
},
|
||||
@@ -609,9 +626,15 @@
|
||||
"current_limit_level": {
|
||||
"name": "Current limit level"
|
||||
},
|
||||
"filter_time": {
|
||||
"name": "Filter time"
|
||||
},
|
||||
"hepa_filter_usage": {
|
||||
"name": "HEPA filter usage"
|
||||
},
|
||||
"outdoor_temperature": {
|
||||
"name": "Outdoor temperature"
|
||||
},
|
||||
"panel_status": {
|
||||
"name": "Panel status"
|
||||
},
|
||||
@@ -736,9 +759,6 @@
|
||||
"hood_filter_usage": {
|
||||
"name": "Filter usage"
|
||||
},
|
||||
"humidity": {
|
||||
"name": "Humidity"
|
||||
},
|
||||
"instance_temperature": {
|
||||
"name": "{instance_name} temperature"
|
||||
},
|
||||
@@ -869,12 +889,18 @@
|
||||
"ai_energy_level": {
|
||||
"name": "AI Energy Mode"
|
||||
},
|
||||
"air_monitoring": {
|
||||
"name": "Air monitoring"
|
||||
},
|
||||
"air_purify": {
|
||||
"name": "Air purification"
|
||||
},
|
||||
"auto_clean": {
|
||||
"name": "Auto clean"
|
||||
},
|
||||
"beep": {
|
||||
"name": "Beep"
|
||||
},
|
||||
"display": {
|
||||
"name": "Display"
|
||||
},
|
||||
@@ -887,6 +913,9 @@
|
||||
"dustbin_auto_close": {
|
||||
"name": "Dustbin auto-close"
|
||||
},
|
||||
"spi": {
|
||||
"name": "Purifying ion"
|
||||
},
|
||||
"uvc_intensive_mode": {
|
||||
"name": "UV-C intensive mode"
|
||||
},
|
||||
|
||||
@@ -120,7 +120,8 @@
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"turbo": "Turbo"
|
||||
"turbo": "Turbo",
|
||||
"max": "Max"
|
||||
}
|
||||
},
|
||||
"preset_mode": {
|
||||
@@ -134,7 +135,8 @@
|
||||
"longwind": "Lange wind",
|
||||
"motionindirect": "Beweging indirect",
|
||||
"motiondirect": "Beweging direct",
|
||||
"drycomfort": "Droog comfort"
|
||||
"drycomfort": "Droog comfort",
|
||||
"2step": "2-Step"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,6 +158,9 @@
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"buzzer_volume": {
|
||||
"name": "Zoemervolume"
|
||||
},
|
||||
"cook_time": {
|
||||
"name": "Bereidingstijd"
|
||||
},
|
||||
@@ -165,6 +170,9 @@
|
||||
"dispense_capacity": {
|
||||
"name": "Schenkhoeveelheid"
|
||||
},
|
||||
"good_sleep": {
|
||||
"name": "Good Sleep"
|
||||
},
|
||||
"instance_setpoint": {
|
||||
"name": "Instelpunt {instance_name}"
|
||||
},
|
||||
@@ -179,12 +187,18 @@
|
||||
},
|
||||
"target_humidity": {
|
||||
"name": "Doelvochtigheid"
|
||||
},
|
||||
"tropical_night_mode": {
|
||||
"name": "Tropische nachtmodus"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"ai_energy_level": {
|
||||
"name": "Niveau AI Energy Mode"
|
||||
},
|
||||
"air_filter_threshold": {
|
||||
"name": "Filteralarmdrempel"
|
||||
},
|
||||
"beverage_zone_mode": {
|
||||
"name": "Modus drankenzone",
|
||||
"state": {
|
||||
@@ -572,6 +586,9 @@
|
||||
"air_filter_usage": {
|
||||
"name": "Filterverbruik"
|
||||
},
|
||||
"air_filter_usage_hours": {
|
||||
"name": "Filterverbruik (uren)"
|
||||
},
|
||||
"air_sensing_state": {
|
||||
"name": "Status luchtmeting"
|
||||
},
|
||||
@@ -609,9 +626,15 @@
|
||||
"current_limit_level": {
|
||||
"name": "Niveau stroombegrenzing"
|
||||
},
|
||||
"filter_time": {
|
||||
"name": "Filtertijd"
|
||||
},
|
||||
"hepa_filter_usage": {
|
||||
"name": "HEPA-filtergebruik"
|
||||
},
|
||||
"outdoor_temperature": {
|
||||
"name": "Buitentemperatuur"
|
||||
},
|
||||
"panel_status": {
|
||||
"name": "Paneelstatus"
|
||||
},
|
||||
@@ -736,9 +759,6 @@
|
||||
"hood_filter_usage": {
|
||||
"name": "Filterverbruik"
|
||||
},
|
||||
"humidity": {
|
||||
"name": "Luchtvochtigheid"
|
||||
},
|
||||
"instance_temperature": {
|
||||
"name": "Temperatuur {instance_name}"
|
||||
},
|
||||
@@ -869,12 +889,18 @@
|
||||
"ai_energy_level": {
|
||||
"name": "AI Energy Mode"
|
||||
},
|
||||
"air_monitoring": {
|
||||
"name": "Luchtmonitoring"
|
||||
},
|
||||
"air_purify": {
|
||||
"name": "Luchtzuivering"
|
||||
},
|
||||
"auto_clean": {
|
||||
"name": "Automatisch reinigen"
|
||||
},
|
||||
"beep": {
|
||||
"name": "Piep"
|
||||
},
|
||||
"display": {
|
||||
"name": "Display"
|
||||
},
|
||||
@@ -887,6 +913,9 @@
|
||||
"dustbin_auto_close": {
|
||||
"name": "Stofreservoir automatisch sluiten"
|
||||
},
|
||||
"spi": {
|
||||
"name": "Ionisatie"
|
||||
},
|
||||
"uvc_intensive_mode": {
|
||||
"name": "Intensieve UV-C-modus"
|
||||
},
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "0000000000",
|
||||
"x.com.samsung.da.countryCode": "VN"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/course/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"HOMECARE_WIZARD_V2"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"DeviceType_015E",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"Course_01",
|
||||
"SteamPush_None",
|
||||
"SeamlessControl_Disable",
|
||||
"KidsLockBypass_On",
|
||||
"WashingTimes_58",
|
||||
"DrumCleanProposal_30",
|
||||
"SpecialFunction_4",
|
||||
"AvailableDelayTime_0",
|
||||
"ProgressTimeSet_AA01E0AC00B4B20690",
|
||||
"WrinklePreventRunning_Off",
|
||||
"SendToDevice_Off",
|
||||
"GMT_12",
|
||||
"Alarm_Off",
|
||||
"SilentSet_0F0FF0F0F0F0F0F0F0F00F0F0F0FF0F0F00FF0",
|
||||
"Silent_Off",
|
||||
"WrinklePreventSet_0F0F0F0F0F0F0F0FF0F00F0F0F0F0F0F0F0FF0",
|
||||
"DelayEndSet_0F0F0F0FF00F0F0F0F0F0F0F0F0F0F0F0F0F0F",
|
||||
"OneTimeCourse_761F00200000000000000000000000",
|
||||
"DownloadCourseList_090A0B12100C140D0F",
|
||||
"EnergyLevelSet_0502020104030204050405020303010302010305",
|
||||
"MostUsed_01",
|
||||
"UsagesDB_ok",
|
||||
"EnergyKW_396",
|
||||
"DrumCleanLog_2024-01-01T00:00:00",
|
||||
"TimeSync_NotSupported"
|
||||
],
|
||||
"x.com.samsung.da.supportedOptions": [
|
||||
"001020403051A1B1C0708"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cycleinterface/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/diagnosis/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.instantaneousPower": "-500",
|
||||
"x.com.samsung.da.cumulativePower": "78200",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.cumulativeDate": "1785272400",
|
||||
"x.com.samsung.da.cumulativeDateUTC": "1785240000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+09:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "DA_DF_A51_20_COMMON|20261441|3801010200131100020100FF00000000",
|
||||
"x.com.samsung.da.description": "DA_DF_A51_20_COMMON_DF8600T/DC92-02656A_0018",
|
||||
"x.com.samsung.da.serialNum": "REDACTED",
|
||||
"x.com.samsung.da.otnDUID": "REDACTED",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "DA_DF_A51_20_COMMON|20261441|3801010200131100020100FF00000000",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02672A230710(E431)",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "DA_DF_A51_20_COMMON",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "20060507,20072702",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.kidsLock": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/0",
|
||||
"rep": {
|
||||
"currentMachineState": "REDACTED",
|
||||
"machineStates": "REDACTED",
|
||||
"jobStates": [
|
||||
"None",
|
||||
"Steaming",
|
||||
"Airwashing",
|
||||
"Drying",
|
||||
"Finish"
|
||||
],
|
||||
"currentJobState": "None",
|
||||
"remainingTime": "00:39:00",
|
||||
"progressPercentage": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.state": "Ready",
|
||||
"x.com.samsung.da.remainingTime": "00:39:00",
|
||||
"x.com.samsung.da.progressPercentage": "1",
|
||||
"x.com.samsung.da.progress": "None",
|
||||
"x.com.samsung.da.delayEndTime": "00:00:00",
|
||||
"x.com.samsung.da.supportedProgress": [
|
||||
"None",
|
||||
"Steaming",
|
||||
"Airwashing",
|
||||
"Drying",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "On"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "10",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.remoteControlEnabled": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/washer/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.wrinklePrevent": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/jobbeginingstatus/vs/0",
|
||||
"rep": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/airdresseroption/sanitize/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sanitize": "Off",
|
||||
"x.com.samsung.da.supportedSanitize": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "3171000000",
|
||||
"x.com.samsung.da.countryCode": "KR"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/course/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"HOMECARE_WIZARD_V2"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"DeviceType_015E",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"Course_22",
|
||||
"AiOption_On",
|
||||
"SteamPush_None",
|
||||
"SeamlessControl_Disable",
|
||||
"KidsLockBypass_On",
|
||||
"FilterUsage_384019D5",
|
||||
"WashingTimes_6",
|
||||
"DrumCleanProposal_30",
|
||||
"ProgressTimeSet_AA01A4AC00F0B20690",
|
||||
"WrinklePreventRunning_Off",
|
||||
"SendToDevice_Off",
|
||||
"WeatherPush_Off",
|
||||
"GMT_12",
|
||||
"Alarm_Off",
|
||||
"WrinklePreventSet_0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0FF00F0F0F0F0F0F0F0F0F0F0FF0F0",
|
||||
"DelayEndSet_0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0FF0F0F00F0F0FF0F0F0F0F00F0F",
|
||||
"EnergyLevelSet_0502010102010202030303010401040505050503030502040503030303030405",
|
||||
"UsagesDB_ok",
|
||||
"DrumCleanLog_2023-04-02T08:08:55|2023-10-02T03:38:19|2024-05-26T03:25:41|2024-10-01T01:07:20|2026-06-16T02:23:29"
|
||||
],
|
||||
"x.com.samsung.da.supportedOptions": [
|
||||
"12261062361020E61020961021261020C61021E61020B61021061020A61061461021361061661022462062562062F62062062040F62042761023061021561021A61021B61021C61022A61022B61022C61022D61022E6102076102086102"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cycleinterface/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cycleInterfaceEnabled": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/diagnosis/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "-500",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePower": "63600",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.cumulativeDate": "1784404800",
|
||||
"x.com.samsung.da.cumulativeDateUTC": "1784372400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+09:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "DA_DF_TP2_20_COMMON|20286141|380101010015110F0201000100010000",
|
||||
"x.com.samsung.da.description": "DA_DF_TP2_20_COMMON_DF9500A/DC92-02888A_0002",
|
||||
"x.com.samsung.da.serialNum": "REDACTED",
|
||||
"x.com.samsung.da.otnDUID": "REDACTED",
|
||||
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "A00",
|
||||
"x.com.samsung.da.diagMinVersion": "1.0",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "DA_DF_TP2_20_COMMON|20286141|380101010015110F0201000100010000",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02673A250416(F822)",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Firmware_1_DB",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "21020115,23030652",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Firmware_2_DB",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "19111852,FFFFFFFF"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.kidsLock": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/0",
|
||||
"rep": {
|
||||
"currentMachineState": "REDACTED",
|
||||
"machineStates": "REDACTED",
|
||||
"jobStates": [
|
||||
"None",
|
||||
"Steaming",
|
||||
"Airwashing",
|
||||
"Drying",
|
||||
"Finish"
|
||||
],
|
||||
"currentJobState": "None",
|
||||
"remainingTime": "00:39:00",
|
||||
"progressPercentage": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.state": "Ready",
|
||||
"x.com.samsung.da.remainingTime": "00:39:00",
|
||||
"x.com.samsung.da.progressPercentage": "1",
|
||||
"x.com.samsung.da.progress": "None",
|
||||
"x.com.samsung.da.delayEndTime": "00:00:00",
|
||||
"x.com.samsung.da.supportedProgress": [
|
||||
"None",
|
||||
"Steaming",
|
||||
"Airwashing",
|
||||
"Drying",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "Micom",
|
||||
"x.com.samsung.da.newVersionAvailable": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.remoteControlEnabled": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/st/airdressercourse/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.st.airdresserMode": "Table_00_Course_22",
|
||||
"x.com.samsung.da.st.courseTable": "Table_00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/washer/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.wrinklePrevent": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/editcourse/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.editCourseList": "EditCourseList_22230C10252F0F0E09121E0B0A14131624202730151A1B1C2A2B2C2D2E0708",
|
||||
"x.com.samsung.da.fixedCourseList": "FixedCourseList_22270F"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/jobbeginingstatus/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/wm/personalcourse/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.courses": [
|
||||
"F1_00",
|
||||
"F2_00",
|
||||
"F3_00",
|
||||
"F4_00",
|
||||
"F5_00",
|
||||
"F6_00",
|
||||
"F7_00",
|
||||
"F8_00",
|
||||
"F9_00",
|
||||
"FA_00"
|
||||
],
|
||||
"x.com.samsung.da.maxCourseNum": "10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/setinfo/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.isModelSettingWithoutSC": "false",
|
||||
"x.com.samsung.da.aiCourse": "false",
|
||||
"x.com.samsung.da.isModelSettingPowerOnOff": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/welcomemsg/vs/0",
|
||||
"rep": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/airlevelcheck/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.periodicSensingActivationState": "Off",
|
||||
"x.com.samsung.da.periodicSensingInterval": "600",
|
||||
"x.com.samsung.da.startSensingOnce": "On",
|
||||
"x.com.samsung.da.sensingState": "NonProcessing",
|
||||
"x.com.samsung.da.lastSensingTime": "1785195000",
|
||||
"x.com.samsung.da.lastSensingLevel": "Kr1",
|
||||
"x.com.samsung.da.autoExeState": "Off",
|
||||
"x.com.samsung.da.supportedAutoExeState": [
|
||||
"Off",
|
||||
"Airpurify",
|
||||
"Alarm"
|
||||
],
|
||||
"x.com.samsung.da.periodicSensingSkipStatus": "Off",
|
||||
"x.com.samsung.da.periodicSensingSkipTime": "15002100"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2024-01-01T00:00:00",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/availablecontrolsets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "0000000000000000000000000000",
|
||||
"x.com.samsung.da.id": "VTL",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "3017000000",
|
||||
"x.com.samsung.da.airconOptionList": [
|
||||
"WELCOMECARE",
|
||||
"AI_PURIFY"
|
||||
],
|
||||
"x.com.samsung.da.countryCode": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/da/softreset/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.softwarereset": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/devicespecificinfo/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.deviceActive": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/dnd/autosleep/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.startTime": "14:00:00",
|
||||
"x.com.samsung.da.endTime": "22:00:00",
|
||||
"x.com.samsung.da.visible": "true",
|
||||
"x.com.samsung.da.useTimeSetting": "true",
|
||||
"x.com.samsung.da.functionState": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cumulativePower": "196696",
|
||||
"x.com.samsung.da.cumulativeDate": "1785245199",
|
||||
"x.com.samsung.da.cumulativeDateUTC": "1785212799",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+09:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/hepafilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsage": "0",
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterStatus": "normal",
|
||||
"x.com.samsung.da.filterCapacity": "8760",
|
||||
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"replaceable"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "A-VTWW-TP2-21-COMMON|10231241|70000535001511EC0505090201000000",
|
||||
"x.com.samsung.da.description": "A-VTWW-TP2-21-COMMON",
|
||||
"x.com.samsung.da.serialNum": "REDACTED",
|
||||
"x.com.samsung.da.otnDUID": "REDACTED",
|
||||
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "101",
|
||||
"x.com.samsung.da.diagMinVersion": "1.0",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02308A260310",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "21063003,21063002",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/convenient/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.options": [
|
||||
"CountOfSlave_0",
|
||||
"SlavePower01_NotSupported",
|
||||
"Pollution_Off",
|
||||
"OptionCode_23418",
|
||||
"SmartSleep_Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"x.com.samsung.da.newVersionNo": "00000000",
|
||||
"x.com.samsung.da.currentVersionInfo": "00000000",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "",
|
||||
"otnTarget": "main",
|
||||
"otnCompleteDate": "noHistory",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "A-VTWW-TP2-21-COMMON",
|
||||
"versions": [
|
||||
"12260310"
|
||||
],
|
||||
"visVersion": "260310"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "09501023124110231341",
|
||||
"versions": [
|
||||
"21063003",
|
||||
"21063002"
|
||||
],
|
||||
"visVersion": "210630"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "7",
|
||||
"x.com.samsung.da.longnotisubscription": "true",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sensors/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Sensor for Dust",
|
||||
"x.com.samsung.da.type": "Dust",
|
||||
"x.com.samsung.da.value": [
|
||||
"5",
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Sensor for FineDust",
|
||||
"x.com.samsung.da.type": "FineDust",
|
||||
"x.com.samsung.da.value": [
|
||||
"5",
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Sensor for Odor",
|
||||
"x.com.samsung.da.type": "Odor",
|
||||
"x.com.samsung.da.value": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "3",
|
||||
"x.com.samsung.da.description": "Sensor for CleanLevel",
|
||||
"x.com.samsung.da.type": "CleanLevel",
|
||||
"x.com.samsung.da.value": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "4",
|
||||
"x.com.samsung.da.description": "Sensor for SuperFineDust",
|
||||
"x.com.samsung.da.type": "SuperFineDust",
|
||||
"x.com.samsung.da.value": [
|
||||
"5",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
],
|
||||
"x.com.samsung.da.total": "5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Seoul",
|
||||
"offset": "+09:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/strength/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "87",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"87",
|
||||
"89",
|
||||
"90",
|
||||
"91"
|
||||
],
|
||||
"x.com.samsung.da.modesName": [
|
||||
"SMART",
|
||||
"MAX",
|
||||
"WINDFREE",
|
||||
"Sleep"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/airflow/0",
|
||||
"rep": {
|
||||
"speed": 3,
|
||||
"direction": "Fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/airflow/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.speedLevel": "3",
|
||||
"x.com.samsung.da.direction": "Fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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-28T00:18:15",
|
||||
"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-28T00:18:15",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "0000000000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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.cumulativePower": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+01:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/0",
|
||||
"rep": {
|
||||
"humidity": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.humidity": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "ARTIK051_KRAC_18K|10193441|60010119001111110200000000000000",
|
||||
"x.com.samsung.da.description": "ARTIK051_KRAC_18K",
|
||||
"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": "02016A200825",
|
||||
"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": "18020800,17120500",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/0",
|
||||
"rep": {
|
||||
"supportedModes": [
|
||||
"Cool",
|
||||
"Dry",
|
||||
"Wind",
|
||||
"Auto",
|
||||
"Heat",
|
||||
"HOMECARE_WIZARD_V2"
|
||||
],
|
||||
"modes": [
|
||||
"Cool"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Cool",
|
||||
"Dry",
|
||||
"Wind",
|
||||
"Auto",
|
||||
"Heat",
|
||||
"HOMECARE_WIZARD_V2"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"Cool"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"Comode_Off",
|
||||
"Sleep_0",
|
||||
"OutdoorTemp_74",
|
||||
"CoolCapa_25",
|
||||
"WarmCapa_32",
|
||||
"Spi_Off",
|
||||
"Autoclean_Off",
|
||||
"Light_Off",
|
||||
"Volume_100",
|
||||
"AirMonitoring_Off",
|
||||
"AutocleanProgress_1",
|
||||
"StopAutoClean_Idle",
|
||||
"FilterTime_1715",
|
||||
"FilterAlarmTime_500",
|
||||
"OptionCode_35882",
|
||||
"ExtendOptionCode_7",
|
||||
"RacInfo_None",
|
||||
"UpdateAllow_NotAllowed"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": "/temperature/current/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
16.0,
|
||||
30.0
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 21.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
16.0,
|
||||
30.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": "Temperature",
|
||||
"x.com.samsung.da.desired": "20",
|
||||
"x.com.samsung.da.current": "21",
|
||||
"x.com.samsung.da.maximum": "30",
|
||||
"x.com.samsung.da.minimum": "16",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+446
@@ -0,0 +1,446 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2024-01-01T00:00:00"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "AC_V_0002_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2024-01-01T00:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/availablecontrolsets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "000000B4012C0000404B0C000000",
|
||||
"x.com.samsung.da.id": "FAC",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "2824500000",
|
||||
"x.com.samsung.da.airconOptionList": [
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"ENERGY_2.0",
|
||||
"AI_2.0",
|
||||
"DeviceTypeMaster",
|
||||
"SingleCommand_1"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/diagnosis/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/0",
|
||||
"rep": {
|
||||
"DRLevel": 0,
|
||||
"start": "1970-01-01T00:00:00Z",
|
||||
"duration": 0,
|
||||
"override": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.drlcLevel": "0",
|
||||
"x.com.samsung.da.duration": "00:00:00",
|
||||
"x.com.samsung.da.drlcStartTime": "1970-01-01T00:00:00Z",
|
||||
"x.com.samsung.da.override": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {
|
||||
"energy": 100.0,
|
||||
"power": 65278.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cumulativeConsumption": "100.000000",
|
||||
"x.com.samsung.da.instantaneousPower": "65278.000000",
|
||||
"x.com.samsung.da.usageThreshold": "0.000000",
|
||||
"x.com.samsung.da.cumulativePower": "1278818",
|
||||
"x.com.samsung.da.cumulativeDate": "1785243600",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePowerType": "individual"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+09:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/airdustfilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsage": "100",
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterDesiredUsage": "112",
|
||||
"x.com.samsung.da.filterStatus": "wash",
|
||||
"x.com.samsung.da.filterCapacity": "112",
|
||||
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"washable"
|
||||
],
|
||||
"x.com.samsung.da.supportedFilterDesiredUsage": [
|
||||
"112",
|
||||
"224",
|
||||
"336",
|
||||
"448"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/0",
|
||||
"rep": {
|
||||
"humidity": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.humidity": "0.000000",
|
||||
"x.com.samsung.da.fivepercentHumidity": "73"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "TP2X_FAC_BORA_21K|10233041|600001110015110006000C1200830000",
|
||||
"x.com.samsung.da.description": "TP2X_FAC_BORA_21K",
|
||||
"x.com.samsung.da.serialNum": "REDACTED",
|
||||
"x.com.samsung.da.otnDUID": "REDACTED",
|
||||
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "000",
|
||||
"x.com.samsung.da.diagMinVersion": "1.0",
|
||||
"x.com.samsung.da.serialNumOption": "REDACTED",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02337A260424",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "2102240021022200",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Outdoor",
|
||||
"x.com.samsung.da.number": "2103300110000300"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/keepnormalstate/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.keepnormal": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/convenient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Off",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Off",
|
||||
"Sleep",
|
||||
"Quiet",
|
||||
"Speed"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"AIComfort",
|
||||
"Cool",
|
||||
"Dry",
|
||||
"Wind"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"AIComfort"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"Operation_Family",
|
||||
"Blooming_0",
|
||||
"OnTimer_0",
|
||||
"OffTimer_0",
|
||||
"Sleep_16",
|
||||
"ArtificialWorking_Off",
|
||||
"ComfortAICooling_Off",
|
||||
"AiTempChanged_Off",
|
||||
"AiTemp_270",
|
||||
"welcomecare_Off",
|
||||
"Panel_Close",
|
||||
"Weather_Off",
|
||||
"Volume_100",
|
||||
"StopAutoClean_Idle",
|
||||
"DiagnosisAI_Off",
|
||||
"Display_Off",
|
||||
"ProgressDiagnosisAI_1",
|
||||
"ResultDiagnosisAI_Normal",
|
||||
"Service_Off",
|
||||
"SmartCoolClean_Off",
|
||||
"ProgressSmartClean_0",
|
||||
"OutDoorVentil_Off",
|
||||
"FreezeAlarmSetting_On",
|
||||
"DesiredFreezeAlarm_112",
|
||||
"OptionCode_529",
|
||||
"ExtendOptionCode_16975",
|
||||
"RacInfo_First",
|
||||
"RacInfo_None_Second",
|
||||
"ModelInfo_16K_BORA_VENT2",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"EnergySaveIcon_Off",
|
||||
"DurationOn_0",
|
||||
"welcomecareElapsedTime_0",
|
||||
"welcomecareThresholdTemp_0",
|
||||
"welcomecareStartDate_0000",
|
||||
"welcomecareEndDate_0000",
|
||||
"welcomecareSeason_None"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/autoclean/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Stop",
|
||||
"x.com.samsung.da.settingStatus": "On",
|
||||
"x.com.samsung.da.progress": "0",
|
||||
"x.com.samsung.da.supportedStatus": [
|
||||
"Start",
|
||||
"SpeedClean",
|
||||
"QuietClean",
|
||||
"Stop"
|
||||
],
|
||||
"x.com.samsung.da.supportedSettingStatus": [
|
||||
"On",
|
||||
"SpeedClean",
|
||||
"QuietClean",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"x.com.samsung.da.newVersionNo": "00000000",
|
||||
"x.com.samsung.da.currentVersionInfo": "00000000",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "",
|
||||
"otnTarget": "main",
|
||||
"otnCompleteDate": "noHistory",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "AFA-KR-TP2-21-AF9X00",
|
||||
"versions": [
|
||||
"10260424"
|
||||
],
|
||||
"visVersion": "260424"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "04511023304110232941",
|
||||
"versions": [
|
||||
"21022400",
|
||||
"21022200"
|
||||
],
|
||||
"visVersion": "210224"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "04511022974110229941",
|
||||
"versions": [
|
||||
"21033001",
|
||||
"10000300"
|
||||
],
|
||||
"visVersion": "210330"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "045110230741FFFFFFFF",
|
||||
"versions": [
|
||||
"22050300",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "220503"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/personality/presence/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "",
|
||||
"x.com.samsung.da.deviceId": "REDACTED",
|
||||
"x.com.samsung.da.value": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.longnotisubscription": "true",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/runn/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.runningMode": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/subdevices/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.subdeviceIdList": "REDACTED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/control/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.increment": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/current/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
18.0,
|
||||
30.0
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 31.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
18.0,
|
||||
30.0
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 27.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Temperature",
|
||||
"x.com.samsung.da.desired": "27.0",
|
||||
"x.com.samsung.da.current": "31.0",
|
||||
"x.com.samsung.da.maximum": "30",
|
||||
"x.com.samsung.da.minimum": "18",
|
||||
"x.com.samsung.da.increment": "1.0",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Seoul",
|
||||
"offset": "+09:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/direction/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "NotSupported",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"NotSupported"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/strength/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "0",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"0",
|
||||
"2",
|
||||
"3",
|
||||
"4"
|
||||
],
|
||||
"x.com.samsung.da.modesName": [
|
||||
"Auto",
|
||||
"Mid",
|
||||
"High",
|
||||
"Turbo"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/aisleep/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.displayNightMode": "Off",
|
||||
"x.com.samsung.da.elapsedTime": "0",
|
||||
"x.com.samsung.da.requestFeedback": "Off",
|
||||
"x.com.samsung.da.resultFeedback": "0",
|
||||
"x.com.samsung.da.statusFeedback": "Idle",
|
||||
"x.com.samsung.da.sleepTime": "14002200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2024-01-01T00:00:00",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "FilterAlarm_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2024-01-01T00:00:00",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/availablecontrolsets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "1DBF00A0012C3F61024804000000",
|
||||
"x.com.samsung.da.id": "RAC",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "1150000000",
|
||||
"x.com.samsung.da.airconOptionList": [
|
||||
"SingleCommand_1",
|
||||
"DR",
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"AI_RAC_KOREA_COOLONLY_3.0",
|
||||
"AI_3.0",
|
||||
"Auto_To_AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connectionconfig/vs/0",
|
||||
"rep": {
|
||||
"autoReconnectionMinVersion": "1.0",
|
||||
"autoReconnection": "true",
|
||||
"autoReconnectionProtocolType": [
|
||||
"helper_hotspot",
|
||||
"ble_ocf"
|
||||
],
|
||||
"supportedWiFiAuthType": [
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA-PSK",
|
||||
"WPA2-PSK",
|
||||
"SAE"
|
||||
],
|
||||
"supportedWiFiCryptoType": [
|
||||
"TKIP",
|
||||
"AES",
|
||||
"WEP-64",
|
||||
"WEP-128"
|
||||
],
|
||||
"supportedWiFiFreq": [
|
||||
"2.4G"
|
||||
],
|
||||
"calmConnectionCare": {
|
||||
"version": "1.0",
|
||||
"role": [
|
||||
"things"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/0",
|
||||
"rep": {
|
||||
"DRLevel": 0,
|
||||
"start": "1970-01-01T00:00:00Z",
|
||||
"duration": 0,
|
||||
"override": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.drlcLevel": "0",
|
||||
"x.com.samsung.da.duration": "00:00:00",
|
||||
"x.com.samsung.da.drlcStartTime": "1970-01-01T00:00:00Z",
|
||||
"x.com.samsung.da.override": "Off",
|
||||
"x.com.samsung.da.realSaving": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {
|
||||
"power": 338.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "338.000000",
|
||||
"x.com.samsung.da.cumulativePower": "1362848",
|
||||
"x.com.samsung.da.cumulativeSavedPower": "0",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+09:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/airdustfilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsage": "2",
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterDesiredUsage": "500",
|
||||
"x.com.samsung.da.filterStatus": "normal",
|
||||
"x.com.samsung.da.filterCapacity": "500",
|
||||
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"replaceable",
|
||||
"washable"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/0",
|
||||
"rep": {
|
||||
"humidity": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.humidity": "0",
|
||||
"x.com.samsung.da.fivepercentHumidity": "42"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "TP1X_DA-AC-RAC-01001_0000|10250041|6001051C001711014E00482200912000",
|
||||
"x.com.samsung.da.description": "TP1X_DA-AC-RAC-01001_0000",
|
||||
"x.com.samsung.da.serialNum": "REDACTED",
|
||||
"x.com.samsung.da.otnDUID": "REDACTED",
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "AR1",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagTsId": "DA01",
|
||||
"x.com.samsung.da.serialNumOption": "REDACTED",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02646A260327",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "102500A23012700",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "102472A22120800,102528A10000100",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/keepnormalstate/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.keepnormal": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/light/vs/0",
|
||||
"rep": {
|
||||
"mode": "On",
|
||||
"supportedModes": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/convenient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Off",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Off",
|
||||
"Sleep",
|
||||
"Quiet",
|
||||
"Smart",
|
||||
"Nano",
|
||||
"NanoSleep"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"AIComfort",
|
||||
"Cool",
|
||||
"Dry",
|
||||
"Wind"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"Cool"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"Sleep_16",
|
||||
"ArtificialWorking_Off",
|
||||
"ComfortAICooling_Off",
|
||||
"AiTempChanged_Off",
|
||||
"AiTemp_280",
|
||||
"welcomecare_Off",
|
||||
"OutdoorConnection_Connected",
|
||||
"OutdoorTemp_86",
|
||||
"CoolCapa_28",
|
||||
"WarmCapa_0",
|
||||
"Volume_100",
|
||||
"StopAutoClean_Set",
|
||||
"Autoclean_On",
|
||||
"DiagnosisAI_Off",
|
||||
"ProgressDiagnosisAI_0",
|
||||
"ResultDiagnosisAI_Normal",
|
||||
"SmartCoolClean_Off",
|
||||
"ProgressSmartClean_0",
|
||||
"FreezeAlarmSetting_Off",
|
||||
"DesiredFreezeAlarm_240",
|
||||
"WashAlarm_Off",
|
||||
"OptionCode_56440",
|
||||
"ExtendOptionCode_246479",
|
||||
"RacInfo_None",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"DurationOn_115",
|
||||
"welcomecareElapsedTime_0",
|
||||
"welcomecareThresholdTemp_0",
|
||||
"welcomecareStartDate_0000",
|
||||
"welcomecareEndDate_0000",
|
||||
"welcomecareSeason_None"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/autoclean/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Stop",
|
||||
"x.com.samsung.da.settingStatus": "On",
|
||||
"x.com.samsung.da.progress": "0",
|
||||
"x.com.samsung.da.supportedStatus": [
|
||||
"Start",
|
||||
"Stop"
|
||||
],
|
||||
"x.com.samsung.da.supportedSettingStatus": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/muteonce/vs/0",
|
||||
"rep": {
|
||||
"muteonce": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"x.com.samsung.da.newVersionNo": "00000000",
|
||||
"x.com.samsung.da.currentVersionInfo": "00000000",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "",
|
||||
"otnTarget": "main",
|
||||
"otnCompleteDate": "noHistory",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "ARA-WW-TP1-24-ARXX00",
|
||||
"versions": [
|
||||
"11260327"
|
||||
],
|
||||
"visVersion": "260327"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "045210250041FFFFFFFF",
|
||||
"versions": [
|
||||
"23012700",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "230127"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "04521024724110252841",
|
||||
"versions": [
|
||||
"22120800",
|
||||
"10000100"
|
||||
],
|
||||
"visVersion": "221208"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/personality/presence/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "",
|
||||
"x.com.samsung.da.deviceId": "REDACTED",
|
||||
"x.com.samsung.da.value": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "On",
|
||||
"causeSource": "SMTS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/quickcontrol/info/vs/0",
|
||||
"rep": {
|
||||
"supportedVersion": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.longnotisubscription": "true",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotedatacontrol/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Off",
|
||||
"x.com.samsung.da.connectionStatus": "Disconnected"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotedeviceinfo/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.didList": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotetemperature/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.temperature": "",
|
||||
"x.com.samsung.da.unit": "",
|
||||
"x.com.samsung.da.error": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/reserverulesets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "1DFFFFFFFF101E121EFFFFFFFFFFFF01000000012F0100000F000000001C00",
|
||||
"x.com.samsung.da.id": "RAC",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sensors/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/control/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.increment": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/current/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
16,
|
||||
30
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 28.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
16,
|
||||
30
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 25.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Temperature",
|
||||
"x.com.samsung.da.desired": "25.0",
|
||||
"x.com.samsung.da.current": "28.0",
|
||||
"x.com.samsung.da.maximum": "30",
|
||||
"x.com.samsung.da.minimum": "16",
|
||||
"x.com.samsung.da.increment": "1.0",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Seoul",
|
||||
"offset": "+09:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/direction/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Fix",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Fix",
|
||||
"Up_And_Low",
|
||||
"Left_And_Right",
|
||||
"All"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/strength/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "32",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"0",
|
||||
"31",
|
||||
"32",
|
||||
"33",
|
||||
"34",
|
||||
"35"
|
||||
],
|
||||
"x.com.samsung.da.modesName": [
|
||||
"Auto",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"MAX"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wirelessinfo/vs/0",
|
||||
"rep": {
|
||||
"macaddressWiFi": "REDACTED",
|
||||
"macaddressBLE": "REDACTED",
|
||||
"connectedApSsid": "REDACTED"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"child_lock",
|
||||
"completion_minutes",
|
||||
"cycle",
|
||||
"cycle_active",
|
||||
"delay_start_hours",
|
||||
"diagnosis_status",
|
||||
"energy_kwh",
|
||||
"finish_time",
|
||||
"job_beginning_status",
|
||||
"machine_state",
|
||||
"power_switch",
|
||||
"progress",
|
||||
"progress_percentage",
|
||||
"remote_control",
|
||||
"wrinkle_prevent"
|
||||
]
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"child_lock",
|
||||
"completion_minutes",
|
||||
"cycle",
|
||||
"cycle_active",
|
||||
"delay_start_hours",
|
||||
"diagnosis_status",
|
||||
"energy_kwh",
|
||||
"finish_time",
|
||||
"firmware_update",
|
||||
"job_beginning_status",
|
||||
"machine_state",
|
||||
"power_switch",
|
||||
"progress",
|
||||
"progress_percentage",
|
||||
"remote_control",
|
||||
"sanitize",
|
||||
"wrinkle_prevent"
|
||||
]
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"clean_level",
|
||||
"device_active",
|
||||
"dust",
|
||||
"energy_kwh",
|
||||
"fine_dust",
|
||||
"firmware_update",
|
||||
"hepa_filter_status",
|
||||
"hepa_filter_usage",
|
||||
"odor",
|
||||
"power_switch",
|
||||
"super_fine_dust",
|
||||
"wind_strength_fan"
|
||||
]
|
||||
}
|
||||
+9
-1
@@ -2,16 +2,24 @@
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"air_filter_usage_hours",
|
||||
"air_purify",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"beep",
|
||||
"clean_level",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"diagnosis_status",
|
||||
"display_light",
|
||||
"dust",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"fine_dust",
|
||||
"humidity",
|
||||
"power_watts"
|
||||
"odor",
|
||||
"power_watts",
|
||||
"super_fine_dust",
|
||||
"tropical_night_mode"
|
||||
]
|
||||
}
|
||||
|
||||
+5
-2
@@ -2,8 +2,10 @@
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"air_filter_usage_hours",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"beep",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
@@ -12,6 +14,7 @@
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"power_watts"
|
||||
"power_watts",
|
||||
"tropical_night_mode"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_monitoring",
|
||||
"alarm_code",
|
||||
"auto_clean_legacy",
|
||||
"buzzer_volume",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"diagnosis_status",
|
||||
"display_light",
|
||||
"energy_kwh",
|
||||
"filter_time",
|
||||
"good_sleep",
|
||||
"humidity",
|
||||
"outdoor_temperature",
|
||||
"spi"
|
||||
]
|
||||
}
|
||||
+4
-1
@@ -2,8 +2,10 @@
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"air_filter_usage_hours",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"beep",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"diagnosis_status",
|
||||
@@ -13,6 +15,7 @@
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"power_watts"
|
||||
"power_watts",
|
||||
"tropical_night_mode"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_threshold",
|
||||
"air_filter_usage",
|
||||
"air_filter_usage_hours",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"beep",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"diagnosis_status",
|
||||
"energy_kwh",
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"power_energy_kwh",
|
||||
"power_watts",
|
||||
"tropical_night_mode"
|
||||
]
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_threshold",
|
||||
"air_filter_usage",
|
||||
"air_filter_usage_hours",
|
||||
"air_purify",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"clean_level",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
"dust",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"fine_dust",
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"selfcheck_error",
|
||||
"selfcheck_result",
|
||||
"selfcheck_status"
|
||||
"selfcheck_status",
|
||||
"super_fine_dust",
|
||||
"tropical_night_mode"
|
||||
]
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,10 +1,13 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_threshold",
|
||||
"air_filter_usage",
|
||||
"air_filter_usage_hours",
|
||||
"air_purify",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"beep",
|
||||
"climate",
|
||||
"current_limit_enabled",
|
||||
"current_limit_level",
|
||||
@@ -16,6 +19,7 @@
|
||||
"mute_once",
|
||||
"selfcheck_error",
|
||||
"selfcheck_result",
|
||||
"selfcheck_status"
|
||||
"selfcheck_status",
|
||||
"tropical_night_mode"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"air_filter_usage_hours",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"beep",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"power_watts",
|
||||
"tropical_night_mode"
|
||||
]
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"air_filter_usage_hours",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"beep",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
@@ -12,6 +14,7 @@
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"power_watts"
|
||||
"power_watts",
|
||||
"tropical_night_mode"
|
||||
]
|
||||
}
|
||||
|
||||
+4
-1
@@ -2,8 +2,10 @@
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"air_filter_usage_hours",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"beep",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
@@ -11,6 +13,7 @@
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"power_watts"
|
||||
"power_watts",
|
||||
"tropical_night_mode"
|
||||
]
|
||||
}
|
||||
|
||||
+9
-1
@@ -2,15 +2,23 @@
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"air_filter_usage_hours",
|
||||
"air_purify",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"beep",
|
||||
"clean_level",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"diagnosis_status",
|
||||
"display_light",
|
||||
"dust",
|
||||
"energy_kwh",
|
||||
"fine_dust",
|
||||
"humidity",
|
||||
"power_watts"
|
||||
"odor",
|
||||
"power_watts",
|
||||
"super_fine_dust",
|
||||
"tropical_night_mode"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_threshold",
|
||||
"air_filter_usage",
|
||||
"air_filter_usage_hours",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"beep",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
@@ -16,6 +19,7 @@
|
||||
"overload_protection_mode",
|
||||
"selfcheck_error",
|
||||
"selfcheck_result",
|
||||
"selfcheck_status"
|
||||
"selfcheck_status",
|
||||
"tropical_night_mode"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"air_filter_usage_hours",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"beep",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
|
||||
+2
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_threshold",
|
||||
"air_filter_usage",
|
||||
"air_filter_usage_hours",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"energy_kwh",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"cavity_state",
|
||||
"child_lock",
|
||||
"cloud_connected",
|
||||
"cook_time",
|
||||
"cooking_mode",
|
||||
"cycle_active",
|
||||
"door_open",
|
||||
"energy_kwh",
|
||||
"fan",
|
||||
"finish_time",
|
||||
"firmware_update",
|
||||
"lamp",
|
||||
"machine_state",
|
||||
"operation_time_minutes",
|
||||
"power_level",
|
||||
"progress_percentage",
|
||||
"sound"
|
||||
]
|
||||
}
|
||||
+27
@@ -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,31 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"ai_energy_level",
|
||||
"alarm_code",
|
||||
"auto_door_opener",
|
||||
"brightness_level",
|
||||
"cabinet_light_dim",
|
||||
"cabinet_light_switch",
|
||||
"cooler_setpoint",
|
||||
"cooler_temperature",
|
||||
"day_brightness",
|
||||
"door_alert",
|
||||
"door_cooler_open",
|
||||
"door_freezer_open",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"firmware_update",
|
||||
"freezer_setpoint",
|
||||
"freezer_temperature",
|
||||
"fridge_sound",
|
||||
"night_end",
|
||||
"night_start",
|
||||
"power_energy_kwh",
|
||||
"power_watts",
|
||||
"rapid_freezing",
|
||||
"rapid_fridge",
|
||||
"selfcheck_error",
|
||||
"selfcheck_result",
|
||||
"selfcheck_status"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"rt": [
|
||||
"x.com.samsung.da.alarms"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.s"
|
||||
],
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "OV_E_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2024-01-01T00:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connected/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.connected": "On",
|
||||
"rt": [
|
||||
"x.com.samsung.da.connected"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.s"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/doors/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Door",
|
||||
"x.com.samsung.da.openState": "Close"
|
||||
}
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.doors"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.s"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "-500",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePower": "103700",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/hood/fanspeed/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.hood.fanSpeed": "0",
|
||||
"x.com.samsung.da.hood.supportedFanSpeed": [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4"
|
||||
],
|
||||
"x.com.samsung.da.hood.settableMaxFanSpeed": "4",
|
||||
"x.com.samsung.da.hood.settableMinFanSpeed": "0",
|
||||
"rt": [
|
||||
"x.com.samsung.da.hood.fanSpeed"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "TP1X_DA-KS-MICROWAVE-01051|40475141|50040000011811000A00000000000000",
|
||||
"x.com.samsung.da.description": "ME7500D-/AA2",
|
||||
"x.com.samsung.da.serialNum": "REDACTED",
|
||||
"x.com.samsung.da.otnDUID": "REDACTED",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "24111400",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "04751A23121900",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "04754B23120800",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
],
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "KM5",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagTsId": "DA01"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.kidsLock": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"NoOperation",
|
||||
"MicroWave",
|
||||
"Autocook",
|
||||
"KeepWarm"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"NoOperation"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"DeviceType_ME7500D-/AA2",
|
||||
"TimeAutoSync_On",
|
||||
"weight_LBS",
|
||||
"TimeSystem_12",
|
||||
"Sound_Off",
|
||||
"RemindBeep_Off",
|
||||
"FilterRemind_Off",
|
||||
"Lamp_High"
|
||||
],
|
||||
"x.com.samsung.da.defaultMode": "MicroWave",
|
||||
"rt": [
|
||||
"x.com.samsung.da.mode"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.state": "Ready",
|
||||
"x.com.samsung.da.operationTime": "00:00:00",
|
||||
"x.com.samsung.da.remainingTime": "00:00:00",
|
||||
"x.com.samsung.da.progressPercentage": "0",
|
||||
"rt": [
|
||||
"x.com.samsung.da.operation"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"flashingProgress": "",
|
||||
"otnStatus": "None",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "AKS-WW-TP1-23-MICROWAVE-OTR",
|
||||
"versions": [
|
||||
"40241114"
|
||||
],
|
||||
"visVersion": "241114"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "074240475141FFFFFFFF",
|
||||
"versions": [
|
||||
"23121900",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "231219"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "074240475442FFFFFFFF",
|
||||
"versions": [
|
||||
"23120800",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "231208"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/oven/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.state": "Ready",
|
||||
"x.com.samsung.da.recipe": "00000000000000",
|
||||
"x.com.samsung.da.powerLevel": "0",
|
||||
"rt": [
|
||||
"x.com.samsung.da.oven"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.s"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/quickcontrol/info/vs/0",
|
||||
"rep": {
|
||||
"supportedVersion": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "America/New_York",
|
||||
"offset": "-04:00",
|
||||
"DST": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wirelessinfo/vs/0",
|
||||
"rep": {
|
||||
"macaddressWiFi": "REDACTED",
|
||||
"macaddressBLE": "REDACTED"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"rt": [
|
||||
"x.com.samsung.da.alarms"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.s"
|
||||
],
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "OV_E_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2024-01-01T00:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/connected/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.connected": "On"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cooktopmonitoring/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.warmingCenterState": "Off",
|
||||
"x.com.samsung.da.cooktopMonitoring": "0",
|
||||
"x.com.samsung.da.cooktopRunningState": "Ready",
|
||||
"supportedHoodLampStateList": [
|
||||
"Run",
|
||||
"Ready"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/doors/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Door",
|
||||
"x.com.samsung.da.openState": "Close",
|
||||
"x.com.samsung.da.lock": "Unlock"
|
||||
}
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.doors"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.s"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.kidsLock": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Bake",
|
||||
"Broil",
|
||||
"ConvectionBake",
|
||||
"ConvectionRoast",
|
||||
"KeepWarm",
|
||||
"BreadProof",
|
||||
"AirFryer",
|
||||
"Dehydrate",
|
||||
"SelfClean",
|
||||
"SteamClean"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"NoOperation"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"DeviceType_NE6516A-/AA0",
|
||||
"SettingPossible_0",
|
||||
"UpperLamp_Off",
|
||||
"Sound_Off",
|
||||
"AdjustingTemp_0",
|
||||
"Sabbath_Off",
|
||||
"EnergySaving_On",
|
||||
"BurnerOnAlert_Off"
|
||||
],
|
||||
"x.com.samsung.da.defaultMode": "ConvectionBake",
|
||||
"x.com.samsung.da.modeSpec": "[{\"mode\":\"Bake\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"175\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"350\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\"},{\"mode\":\"ConvectionRoast\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"160\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"325\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\"},{\"mode\":\"AirFryer\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"175\",\"tempMaxC\":\"260\",\"tempDefaultC\":\"220\",\"tempListLengthC\":\"0\",\"tempMinF\":\"350\",\"tempMaxF\":\"500\",\"tempDefaultF\":\"425\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\"}]",
|
||||
"rt": [
|
||||
"x.com.samsung.da.mode"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.state": "Ready",
|
||||
"x.com.samsung.da.operationTime": "00:00:00",
|
||||
"x.com.samsung.da.remainingTime": "00:00:00",
|
||||
"x.com.samsung.da.progressPercentage": "1",
|
||||
"rt": [
|
||||
"x.com.samsung.da.operation"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/oven/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.state": "Ready",
|
||||
"rt": [
|
||||
"x.com.samsung.da.oven"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.s"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "On"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.remoteControlEnabled": "true",
|
||||
"rt": [
|
||||
"x.com.samsung.da.configuration"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.s"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Temperature",
|
||||
"x.com.samsung.da.desired": "0",
|
||||
"x.com.samsung.da.current": "175",
|
||||
"x.com.samsung.da.increment": "1",
|
||||
"x.com.samsung.da.unit": "Fahrenheit"
|
||||
}
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.temperatures"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"rt": [
|
||||
"x.com.samsung.da.alarms"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cabinet/light/enhanced/vs/0",
|
||||
"rep": {
|
||||
"light.control.status": "Off",
|
||||
"level.brightness.daytime": "100",
|
||||
"level.brightness.nighttime": "100",
|
||||
"night.starttime": "2025-08-08T19:00:00",
|
||||
"night.duration.minute": "540",
|
||||
"timezone.offset": "+02:00",
|
||||
"rt": [
|
||||
"x.com.samsung.da.light.enhanced"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cabinet/light/total/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.lightLevel": "100",
|
||||
"x.com.samsung.da.lightResolution": "3",
|
||||
"x.com.samsung.da.lightControl.off.include": "Off",
|
||||
"x.com.samsung.da.lightControl": "Off",
|
||||
"x.com.samsung.da.lightControl.hide": "true",
|
||||
"light.dimming.status": "On",
|
||||
"rt": [
|
||||
"x.com.samsung.da.cabinetlight"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "",
|
||||
"x.com.samsung.da.countryCode": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connectionconfig/vs/0",
|
||||
"rep": {
|
||||
"autoReconnectionMinVersion": "1.0",
|
||||
"autoReconnection": "true",
|
||||
"autoReconnectionProtocolType": [
|
||||
"helper_hotspot",
|
||||
"ble_ocf"
|
||||
],
|
||||
"supportedWiFiAuthType": [
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA-PSK",
|
||||
"WPA2-PSK",
|
||||
"SAE"
|
||||
],
|
||||
"supportedWiFiCryptoType": [
|
||||
"TKIP",
|
||||
"AES",
|
||||
"WEP-64",
|
||||
"WEP-128"
|
||||
],
|
||||
"supportedWiFiFreq": [
|
||||
"2.4G"
|
||||
],
|
||||
"calmConnectionCare": {
|
||||
"version": "1.0",
|
||||
"role": [
|
||||
"things"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/dginformation/vs/0",
|
||||
"rep": {
|
||||
"enrolmentstatus": "Unknown",
|
||||
"devicestate": "Unknown",
|
||||
"lockstatus": "Unknown",
|
||||
"nextduedate": "",
|
||||
"workingminutes": 0,
|
||||
"paymentinfo": {
|
||||
"emiplan": "Unknown",
|
||||
"currency": "Unknown",
|
||||
"totalemi": 0,
|
||||
"totalemipaid": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/door/cooler/0",
|
||||
"rep": {
|
||||
"openState": "Close",
|
||||
"rt": [
|
||||
"oic.r.door"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.s"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/door/freezer/0",
|
||||
"rep": {
|
||||
"openState": "Close",
|
||||
"rt": [
|
||||
"oic.r.door"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.s"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/doors/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.openState": "Close",
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Door"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.openState": "Close",
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Door"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.drlcLevel": "0",
|
||||
"x.com.samsung.da.override": "Not_Supported",
|
||||
"x.com.samsung.da.durationminutes": "0",
|
||||
"x.com.samsung.da.realSaving": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/ailevel/vs/0",
|
||||
"rep": {
|
||||
"aiLevel": "1",
|
||||
"supportedAiLevel": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cumulativeConsumption": "281",
|
||||
"x.com.samsung.da.instantaneousPower": "48",
|
||||
"x.com.samsung.da.cumulativePower": "216620",
|
||||
"x.com.samsung.da.cumulativeSavedPower": "0",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+01:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "TP1X_REF_21K|00175941|00050126001811344100000020090000",
|
||||
"x.com.samsung.da.description": "TP1X_REF_21K",
|
||||
"x.com.samsung.da.serialNum": "REDACTED",
|
||||
"x.com.samsung.da.otnDUID": "REDACTED",
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "RB2",
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagTsId": "DA01",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "WiFi Module",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "260617",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Micom",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "24070109, FFFFFFFF, 24062000, FFFFFFFF",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": [
|
||||
"RVACATION_OFF",
|
||||
"FCONVERT_FREEZER"
|
||||
],
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"ENERGY_REPORT_MODEL",
|
||||
"18K_REF_OUTDOOR_CONTROL_V2"
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.mode"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "0",
|
||||
"otnCompleteDate": "noHistory",
|
||||
"scheduledTime": "None",
|
||||
"swVersionInfo": {
|
||||
"platform": "Tizen Lite",
|
||||
"oneUiVersion": "7.0 Refrigerator",
|
||||
"osVersion": "4.0"
|
||||
},
|
||||
"otnList": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/quickcontrol/info/vs/0",
|
||||
"rep": {
|
||||
"supportedVersion": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/refrigeration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.rapidFridge": "Off",
|
||||
"x.com.samsung.da.rapidFreezing": "Off",
|
||||
"rt": [
|
||||
"x.com.samsung.da.fridge"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/rm/control/vs/0",
|
||||
"rep": {
|
||||
"minPeriod": "9000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/runningmode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.runningMode": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/selfcheck/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedActions": [
|
||||
"Start"
|
||||
],
|
||||
"x.com.samsung.da.status": "Ready",
|
||||
"x.com.samsung.da.result": "Success",
|
||||
"x.com.samsung.da.error": [
|
||||
"ErrorCode_None"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/settings/sound/alert/door/vs/0",
|
||||
"rep": {
|
||||
"alert.door": "1",
|
||||
"supportedAlert.door": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4"
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.alert.door"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/status/lock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.device.sound": "Off",
|
||||
"rt": [
|
||||
"x.com.samsung.da.lockstatus"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/current/cooler/0",
|
||||
"rep": {
|
||||
"temperature": 6.0,
|
||||
"range": [
|
||||
1.0,
|
||||
7.0
|
||||
],
|
||||
"units": "C",
|
||||
"rt": [
|
||||
"oic.r.temperature"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/current/freezer/0",
|
||||
"rep": {
|
||||
"temperature": -17.0,
|
||||
"range": [
|
||||
-23.0,
|
||||
-15.0
|
||||
],
|
||||
"units": "C",
|
||||
"rt": [
|
||||
"oic.r.temperature"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/cooler/0",
|
||||
"rep": {
|
||||
"temperature": 6.0,
|
||||
"range": [
|
||||
1.0,
|
||||
7.0
|
||||
],
|
||||
"units": "C",
|
||||
"rt": [
|
||||
"oic.r.temperature"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/freezer/0",
|
||||
"rep": {
|
||||
"temperature": -17.0,
|
||||
"range": [
|
||||
-23.0,
|
||||
-15.0
|
||||
],
|
||||
"units": "C",
|
||||
"rt": [
|
||||
"oic.r.temperature"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Freezer",
|
||||
"x.com.samsung.da.desired": "-17",
|
||||
"x.com.samsung.da.current": "-17",
|
||||
"x.com.samsung.da.maximum": "-15",
|
||||
"x.com.samsung.da.minimum": "-23",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Fridge",
|
||||
"x.com.samsung.da.desired": "6",
|
||||
"x.com.samsung.da.current": "6",
|
||||
"x.com.samsung.da.maximum": "7",
|
||||
"x.com.samsung.da.minimum": "1",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Europe/Rome",
|
||||
"offset": "+02:00",
|
||||
"DST": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wirelessinfo/vs/0",
|
||||
"rep": {
|
||||
"macaddressWiFi": "REDACTED",
|
||||
"macaddressBLE": "REDACTED",
|
||||
"connectedApSsid": "SPRDFL24"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Tests for the AirDresser device type (DA_DF_A51_20_COMMON, issue #162)."""
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _air_dresser():
|
||||
resources = _load_device('air_dresser')
|
||||
info = resources['/information/vs/0']
|
||||
reg = for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
|
||||
return reg, resources
|
||||
|
||||
|
||||
def _state():
|
||||
reg, resources = _air_dresser()
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||
return flatten(bound, resources)
|
||||
|
||||
|
||||
def test_resolves_to_air_dresser_registry():
|
||||
reg, _ = _air_dresser()
|
||||
assert reg is not None and reg.name == 'air_dresser'
|
||||
|
||||
|
||||
def test_no_unbound_hrefs():
|
||||
reg, resources = _air_dresser()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_course_select_present_and_reads_current_selection():
|
||||
"""This was the actual reported gap: /course/vs/0 was previously
|
||||
entirely unbound, so no cycle/mode select existed at all."""
|
||||
state = _state()
|
||||
assert state['cycle'] == '01'
|
||||
|
||||
|
||||
def test_course_options_derived_from_supported_options_fallback():
|
||||
"""No /wm/editcourse/vs/0 on this board at all, so the option list must
|
||||
come from laundry.cycle_options' supportedOptions decode rather than
|
||||
editCourseList -- confirmed distinct codes, current selection included."""
|
||||
from custom_components.localthings.registry.capabilities.laundry import cycle_options
|
||||
_, resources = _air_dresser()
|
||||
codes = cycle_options(resources)
|
||||
assert codes == ['01', '02', '04', '03', '05', '1A', '1B', '1C', '07', '08']
|
||||
|
||||
|
||||
def test_wrinkle_prevent_present_dry_level_fields_absent():
|
||||
"""AIR_DRESSER_SETTINGS only binds wrinkle_prevent -- dryLevel/dryTime/
|
||||
dryerType are dryer-only fields this device never reports, so they
|
||||
should not appear as always-empty entities."""
|
||||
state = _state()
|
||||
assert state['wrinkle_prevent'] is False
|
||||
for key in ('dry_level', 'dry_time', 'dryer_type'):
|
||||
assert key not in state, key
|
||||
|
||||
|
||||
def test_expected_entities_present():
|
||||
state = _state()
|
||||
for key in (
|
||||
'power_switch', 'child_lock', 'remote_control', 'cycle',
|
||||
'machine_state', 'progress', 'progress_percentage', 'finish_time',
|
||||
'completion_minutes', 'delay_start_hours', 'diagnosis_status',
|
||||
'job_beginning_status', 'wrinkle_prevent', 'energy_kwh',
|
||||
):
|
||||
assert key in state, key
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Tests for the DA_DF_TP2_20_COMMON AirDresser (model DF9500A, issue #157).
|
||||
|
||||
A different board generation than issue #162's DA_DF_A51_20_COMMON, routed
|
||||
into the same air_dresser registry (both carry the '_DF_' modelNum token).
|
||||
Exercises the two things this board does differently: a populated
|
||||
/wm/editcourse/vs/0 (so cycle_options() never needs the supportedOptions
|
||||
fallback #162 relies on) and the new /airdresseroption/sanitize/vs/0
|
||||
capability #162's dump doesn't report at all.
|
||||
"""
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import air_dresser, for_device_by_model
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _air_dresser():
|
||||
resources = _load_device('air_dresser_tp2_20')
|
||||
info = resources['/information/vs/0']
|
||||
reg = for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
|
||||
return reg, resources
|
||||
|
||||
|
||||
def _state():
|
||||
reg, resources = _air_dresser()
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||
return flatten(bound, resources)
|
||||
|
||||
|
||||
def test_resolves_to_air_dresser_registry():
|
||||
reg, _ = _air_dresser()
|
||||
assert reg is not None and reg.name == 'air_dresser'
|
||||
|
||||
|
||||
def test_no_unbound_hrefs():
|
||||
"""Confirms /st/airdressercourse/vs/0 and /airdresseroption/sanitize/vs/0
|
||||
-- the two hrefs this board reports that #162's dump doesn't -- are
|
||||
covered (ignored.py and AIR_DRESSER_SANITIZE respectively)."""
|
||||
reg, resources = _air_dresser()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_course_options_come_from_edit_course_list_not_the_fallback():
|
||||
"""Unlike issue #162's board, this one populates /wm/editcourse/vs/0
|
||||
directly, so cycle_options() should never reach the
|
||||
supportedOptions-decode fallback."""
|
||||
from custom_components.localthings.registry.capabilities.laundry import cycle_options
|
||||
_, resources = _air_dresser()
|
||||
codes = cycle_options(resources)
|
||||
assert codes[:3] == ['22', '23', '0C']
|
||||
assert codes == cycle_options({'/wm/editcourse/vs/0': resources['/wm/editcourse/vs/0']})
|
||||
|
||||
|
||||
def test_course_select_reads_current_selection():
|
||||
state = _state()
|
||||
assert state['cycle'] == '22'
|
||||
|
||||
|
||||
def test_course_translation_key_falls_back_to_cycle_for_unidentified_table():
|
||||
"""Table_00's course codes aren't identified yet, so the select's
|
||||
translation_key resolves to the generic 'cycle' catalog entry rather
|
||||
than a table-specific one that doesn't exist."""
|
||||
_, resources = _air_dresser()
|
||||
desc = air_dresser.REGISTRY.capabilities['/course/vs/0'][0].entities[0]
|
||||
assert desc.translation_key(resources) == 'cycle'
|
||||
|
||||
|
||||
def test_sanitize_present_and_toggles():
|
||||
state = _state()
|
||||
assert state['sanitize'] is False
|
||||
|
||||
desc = next(
|
||||
e for e in air_dresser.REGISTRY.capabilities['/airdresseroption/sanitize/vs/0'][0].entities
|
||||
if e.key == 'sanitize'
|
||||
)
|
||||
path, body = desc.write_fn('On', {})
|
||||
assert path == ['airdresseroption', 'sanitize', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.sanitize': 'On'}
|
||||
assert desc.write_fn('Sparkle', {}) is None
|
||||
@@ -0,0 +1,128 @@
|
||||
"""HA fan-entity mapping tests for the A-VTWW-TP2-21-COMMON BESPOKE Cube Air
|
||||
(issue #151).
|
||||
|
||||
This board's /wind/strength/vs/0 reports numeric wind-strength codes
|
||||
("87"/"89"/"90"/"91") with a separate modesName array ("SMART"/"MAX"/
|
||||
"WINDFREE"/"Sleep") giving the actual names, unlike the TP1X_DA-AC-AIR
|
||||
family's /mode/vs/0 (issue #130) where supportedModes IS the name list
|
||||
already. LocalThingsAirPurifierFan._label_for_code resolves both shapes
|
||||
without a per-model map.
|
||||
"""
|
||||
from custom_components.localthings.fan import LocalThingsAirPurifierFan
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
from custom_components.localthings.registry.capabilities.air_purifier import HREF_WIND_STRENGTH
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.entities import FanDesc
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
class _FakeCoordinator:
|
||||
device_serial = 'TEST-VTWW-SERIAL'
|
||||
device_info = {}
|
||||
data = {}
|
||||
|
||||
def __init__(self, resources):
|
||||
self.last_resources = resources
|
||||
self.commands = []
|
||||
|
||||
def resource(self, href):
|
||||
return self.last_resources.get(href, {})
|
||||
|
||||
async def async_send_command(self, bound, payload):
|
||||
self.commands.append((bound, payload))
|
||||
|
||||
|
||||
def _resources():
|
||||
return _load_device('air_purifier_vtww')
|
||||
|
||||
|
||||
def _reg(resources):
|
||||
info = resources['/information/vs/0']
|
||||
return for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
|
||||
|
||||
|
||||
def _entity(resources, coordinator=None):
|
||||
reg = _reg(resources)
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||
fan_bound = next(
|
||||
item for item in bound
|
||||
if isinstance(item.desc, FanDesc) and item.href == HREF_WIND_STRENGTH
|
||||
)
|
||||
return LocalThingsAirPurifierFan(coordinator or _FakeCoordinator(resources), fan_bound)
|
||||
|
||||
|
||||
def test_resolves_to_air_purifier_registry():
|
||||
assert _reg(_resources()).name == 'air_purifier'
|
||||
|
||||
|
||||
def test_no_unbound_hrefs():
|
||||
resources = _resources()
|
||||
reg = _reg(resources)
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_preset_modes_come_from_modes_name_not_the_raw_codes():
|
||||
entity = _entity(_resources())
|
||||
assert entity.preset_modes == ['smart', 'max', 'windfree', 'sleep']
|
||||
|
||||
|
||||
def test_preset_mode_reads_the_current_code_via_modes_name():
|
||||
"""Fixture's current mode is '87' -> modesName[0] 'SMART'."""
|
||||
entity = _entity(_resources())
|
||||
assert entity.preset_mode == 'smart'
|
||||
|
||||
|
||||
async def test_set_preset_mode_writes_back_the_raw_code():
|
||||
resources = _resources()
|
||||
coordinator = _FakeCoordinator(resources)
|
||||
entity = _entity(resources, coordinator)
|
||||
|
||||
await entity.async_set_preset_mode('windfree')
|
||||
|
||||
assert coordinator.commands[-1][1] == ('mode', '90')
|
||||
|
||||
|
||||
def test_is_on_reads_vendor_power():
|
||||
entity = _entity(_resources())
|
||||
assert entity.is_on is False
|
||||
|
||||
|
||||
def test_wind_strength_fan_key_does_not_collide_with_mode_fan():
|
||||
"""WIND_STRENGTH_FAN and FAN both live in this registry and both are
|
||||
FanDesc-typed; BoundEntity's unique_id is built from key alone (entity.py's
|
||||
_key), not href, so a shared key would silently shadow one entity if a
|
||||
board ever bound both (see AIRFLOW_GENERIC's own comment on this exact
|
||||
hazard). No real dump reports both hrefs today, but the keys must stay
|
||||
distinct regardless."""
|
||||
from custom_components.localthings.registry.capabilities import air_purifier
|
||||
|
||||
fan_keys = {
|
||||
entity.key
|
||||
for cap in (air_purifier.FAN, air_purifier.WIND_STRENGTH_FAN)
|
||||
for entity in cap.entities
|
||||
}
|
||||
assert fan_keys == {'fan', 'wind_strength_fan'}
|
||||
|
||||
|
||||
def test_both_fan_hrefs_bound_simultaneously_produce_distinct_entities():
|
||||
"""Synthetic combination (no real dump reports both hrefs) proving the
|
||||
two FanDescs don't shadow each other in flatten()'s key-based state dict
|
||||
even if a future board did report both."""
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
|
||||
resources = _resources()
|
||||
resources['/mode/vs/0'] = {
|
||||
'x.com.samsung.da.modes': ['Smart'],
|
||||
'x.com.samsung.da.supportedModes': ['Smart', 'Max', 'Mid', 'WindFree', 'Sleep'],
|
||||
}
|
||||
reg = _reg(resources)
|
||||
unbound = []
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
state = flatten(bound, resources)
|
||||
assert state['fan'] == 'Smart'
|
||||
assert state['wind_strength_fan'] == '87'
|
||||
@@ -0,0 +1,251 @@
|
||||
"""ARTIK051_KRAC_18K room air conditioner (issue #136).
|
||||
|
||||
This board generation predates every AC dump the registry was built from and
|
||||
differs from all of them in three ways, each covered below:
|
||||
|
||||
* No ``/wind/*`` resources at all -- fan speed and vane direction share one
|
||||
``/airflow/vs/0`` resource (``speedLevel`` on the same 0-4 scale as
|
||||
``_DEVICE_TO_FAN``, ``direction`` with the same codes as ``_DEVICE_TO_SWING``).
|
||||
* No ``/mode/convenient/vs/0`` -- the convenient-mode preset is a ``Comode_*``
|
||||
token in ``/mode/vs/0``'s ``options`` array.
|
||||
* Several settings (SPI, auto clean, air monitoring, beep volume, Good Sleep,
|
||||
outdoor temperature, filter time) are ``options`` tokens too, where newer
|
||||
boards have dedicated resources.
|
||||
|
||||
Fan, swing, preset, SPI and beep-volume writes were all confirmed on hardware
|
||||
by read-back on the unit this fixture is dumped from. The issue #136 unit is
|
||||
the same model with a slightly different token set (no ``Spi``,
|
||||
``FilterTime_5460``, ``OutdoorTemp_81``), which the token entities' presence
|
||||
gating handles the same way it handles newer boards.
|
||||
"""
|
||||
from custom_components.localthings.climate import LocalThingsClimate
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import (
|
||||
airconditioner, for_device_by_model,
|
||||
)
|
||||
from custom_components.localthings.registry.capabilities.airconditioner import (
|
||||
HREF_AIRFLOW, HREF_WIND_STRENGTH, _option_number_write,
|
||||
_option_switch_write, is_legacy_board,
|
||||
)
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.entities import ClimateDesc
|
||||
from tests.conftest import _load_device
|
||||
|
||||
FIXTURE = 'airconditioner_artik051_krac_18k'
|
||||
MODEL = 'ARTIK051_KRAC_18K|10193441|60010119001111010100000000000000'
|
||||
|
||||
|
||||
class _FakeCoordinator:
|
||||
device_serial = 'TEST-KRAC-SERIAL'
|
||||
device_info = {}
|
||||
data = {}
|
||||
|
||||
def __init__(self, resources):
|
||||
self.last_resources = resources
|
||||
self.commands = []
|
||||
|
||||
def resource(self, href):
|
||||
return self.last_resources.get(href, {})
|
||||
|
||||
async def async_send_command(self, bound, payload):
|
||||
self.commands.append((bound, payload))
|
||||
|
||||
|
||||
def _discover(resources, registry=airconditioner.REGISTRY):
|
||||
unbound = []
|
||||
bound = discover(resources, registry.capabilities,
|
||||
registry.pattern_capabilities, log=unbound.append)
|
||||
return bound, unbound
|
||||
|
||||
|
||||
def _state(fixture=FIXTURE):
|
||||
resources = _load_device(fixture)
|
||||
bound, _ = _discover(resources)
|
||||
return flatten(bound, resources)
|
||||
|
||||
|
||||
def _climate(resources, coordinator=None):
|
||||
bound, _ = _discover(resources)
|
||||
climate_bound = next(
|
||||
item for item in bound if isinstance(item.desc, ClimateDesc)
|
||||
)
|
||||
return LocalThingsClimate(
|
||||
coordinator or _FakeCoordinator(resources), climate_bound,
|
||||
)
|
||||
|
||||
|
||||
# -- device type --------------------------------------------------------------
|
||||
|
||||
def test_krac_model_resolves_to_the_airconditioner_registry():
|
||||
"""The '_RAC_' token check can't see '_KRAC_' -- the 'K' sits between the
|
||||
underscore and 'RAC' -- and the consumer-prefix fallback only covers
|
||||
washers/dryers/dishwashers, so this model resolved to 'unknown' and
|
||||
exposed nothing but power."""
|
||||
registry = for_device_by_model(MODEL, 'ARTIK051_KRAC_18K')
|
||||
assert registry is not None
|
||||
assert registry.name == 'airconditioner'
|
||||
|
||||
|
||||
def test_no_unbound_hrefs():
|
||||
_, unbound = _discover(_load_device(FIXTURE))
|
||||
assert unbound == []
|
||||
|
||||
|
||||
# -- option-token entities ----------------------------------------------------
|
||||
|
||||
def test_token_entities_present_with_calibrated_values():
|
||||
state = _state()
|
||||
# token/10 hours: 1715 displayed as "171 hours 0 minutes"... at 1710 in the
|
||||
# official app on this unit, which pins the scale (the .5 here is a later
|
||||
# reading). Whether it counts up or down is deliberately not asserted --
|
||||
# see the descriptor comment.
|
||||
assert state['filter_time'] == 171.5
|
||||
# token - 55 == 19 C, against a 19.4 C forecast at the time of the dump.
|
||||
assert state['outdoor_temperature'] == 19.0
|
||||
assert state['buzzer_volume'] == 100.0
|
||||
assert state['good_sleep'] == 0.0
|
||||
assert state['spi'] is False
|
||||
assert state['auto_clean_legacy'] is False
|
||||
assert state['air_monitoring'] is False
|
||||
|
||||
|
||||
def test_token_entities_stay_off_newer_boards():
|
||||
"""Newer families carry Volume/Sleep/OutdoorTemp/Autoclean tokens too,
|
||||
while also exposing those settings as dedicated resources -- ungated, the
|
||||
token entities would duplicate them (auto clean) or apply a scale
|
||||
calibrated on another board generation (outdoor temperature)."""
|
||||
state = _state('airconditioner_tp1x_rac')
|
||||
for key in ('spi', 'auto_clean_legacy', 'air_monitoring', 'buzzer_volume',
|
||||
'good_sleep', 'outdoor_temperature', 'filter_time'):
|
||||
assert key not in state, key
|
||||
|
||||
|
||||
def test_climate_legacy_airflow_gate_agrees_with_is_legacy_board():
|
||||
"""issue #161: climate.py's _legacy_airflow() delegates to
|
||||
capabilities/airconditioner.py's is_legacy_board() instead of
|
||||
re-implementing the same presence/absence check, so the token entities
|
||||
and the climate card's legacy read/write paths can't drift apart on
|
||||
which board generation is in play."""
|
||||
legacy_resources = _load_device(FIXTURE)
|
||||
assert is_legacy_board(legacy_resources) is True
|
||||
assert _climate(legacy_resources)._legacy_airflow() == legacy_resources[HREF_AIRFLOW]
|
||||
|
||||
newer_resources = _load_device('airconditioner_tp1x_rac')
|
||||
assert is_legacy_board(newer_resources) is False
|
||||
assert _climate(newer_resources)._legacy_airflow() == {}
|
||||
assert HREF_WIND_STRENGTH in newer_resources
|
||||
|
||||
|
||||
def test_absent_token_yields_no_entity():
|
||||
"""The issue #136 unit of this same model reports no Spi token."""
|
||||
resources = _load_device(FIXTURE)
|
||||
options = resources['/mode/vs/0']['x.com.samsung.da.options']
|
||||
resources['/mode/vs/0']['x.com.samsung.da.options'] = [
|
||||
option for option in options if not option.startswith('Spi_')
|
||||
]
|
||||
bound, _ = _discover(resources)
|
||||
assert 'spi' not in flatten(bound, resources)
|
||||
|
||||
|
||||
def test_humidity_reads_the_vendor_field_and_treats_zero_as_unknown():
|
||||
"""This board has no fivepercentHumidity field; the plain humidity field
|
||||
only carries a reading while Air monitoring is on, and the unit switches
|
||||
that back off by itself after about a minute."""
|
||||
resources = _load_device(FIXTURE)
|
||||
assert resources['/humidity/vs/0']['x.com.samsung.da.humidity'] == '0'
|
||||
bound, _ = _discover(resources)
|
||||
assert flatten(bound, resources)['humidity'] is None
|
||||
|
||||
resources['/humidity/vs/0']['x.com.samsung.da.humidity'] = '51'
|
||||
bound, _ = _discover(resources)
|
||||
assert flatten(bound, resources)['humidity'] == 51.0
|
||||
|
||||
|
||||
def test_option_writes_carry_one_token():
|
||||
"""Both go through option_write's single-token merge, the same mechanism
|
||||
the display light already uses on this href. Confirmed on hardware by
|
||||
read-back: Spi_On/Spi_Off, and the volume token surviving a write."""
|
||||
assert _option_switch_write('Spi')('On', {}) == (
|
||||
['mode', 'vs', '0'], {'x.com.samsung.da.options': ['Spi_On']},
|
||||
)
|
||||
# The number platform hands over a float; the device wants an integer token.
|
||||
assert _option_number_write('Volume')(70.0, {}) == (
|
||||
['mode', 'vs', '0'], {'x.com.samsung.da.options': ['Volume_70']},
|
||||
)
|
||||
|
||||
|
||||
# -- climate entity: fan, swing and preset off /airflow/vs/0 ------------------
|
||||
|
||||
def test_fan_mode_reads_the_airflow_speed_level():
|
||||
entity = _climate(_load_device(FIXTURE))
|
||||
assert entity.fan_mode == 'high' # speedLevel 3 in the fixture
|
||||
# No supportedModes on this resource, so the full 0-4 scale is offered.
|
||||
assert entity.fan_modes == ['auto', 'low', 'medium', 'high', 'turbo']
|
||||
|
||||
|
||||
def test_swing_mode_reads_the_airflow_direction():
|
||||
resources = _load_device(FIXTURE)
|
||||
entity = _climate(resources)
|
||||
assert entity.swing_mode == 'off' # 'Fix' in the fixture
|
||||
|
||||
resources[HREF_AIRFLOW]['x.com.samsung.da.direction'] = 'All'
|
||||
assert _climate(resources).swing_mode == 'both'
|
||||
assert 'both' in _climate(resources).swing_modes
|
||||
|
||||
|
||||
async def test_fan_and_swing_writes_target_the_airflow_resource():
|
||||
resources = _load_device(FIXTURE)
|
||||
coordinator = _FakeCoordinator(resources)
|
||||
entity = _climate(resources, coordinator)
|
||||
|
||||
await entity.async_set_fan_mode('turbo')
|
||||
await entity.async_set_swing_mode('both')
|
||||
|
||||
assert [payload for _, payload in coordinator.commands] == [
|
||||
('fan_legacy', '4'), ('swing_legacy', 'All'),
|
||||
]
|
||||
|
||||
|
||||
def test_preset_comes_from_the_comode_token():
|
||||
resources = _load_device(FIXTURE)
|
||||
entity = _climate(resources)
|
||||
assert entity.preset_mode == 'none' # Comode_Off in the fixture
|
||||
# Codes learned by driving this unit through its cloud integration and
|
||||
# reading the token back. They go through the same dynamic resolver as a
|
||||
# real convenient resource's supportedModes, so 'Nano' resolves to the
|
||||
# existing 'nano' preset -- already labelled WindFree in the catalog.
|
||||
assert entity.preset_modes == [
|
||||
'none', 'nano', 'quiet', 'comfort', '2step', 'speed',
|
||||
]
|
||||
|
||||
options = resources['/mode/vs/0']['x.com.samsung.da.options']
|
||||
resources['/mode/vs/0']['x.com.samsung.da.options'] = [
|
||||
'Comode_Nano' if option.startswith('Comode_') else option
|
||||
for option in options
|
||||
]
|
||||
assert _climate(resources).preset_mode == 'nano'
|
||||
|
||||
|
||||
async def test_preset_write_uses_the_token_path():
|
||||
resources = _load_device(FIXTURE)
|
||||
coordinator = _FakeCoordinator(resources)
|
||||
entity = _climate(resources, coordinator)
|
||||
|
||||
await entity.async_set_preset_mode('nano')
|
||||
|
||||
assert coordinator.commands[-1][1] == ('preset_legacy', 'Nano')
|
||||
|
||||
|
||||
async def test_newer_boards_keep_the_resource_paths():
|
||||
"""The legacy fallbacks are gated on this board's resource shape, so a
|
||||
board with /wind/* and /mode/convenient/vs/0 must be untouched by them."""
|
||||
resources = _load_device('airconditioner_tp1x_rac')
|
||||
coordinator = _FakeCoordinator(resources)
|
||||
entity = _climate(resources, coordinator)
|
||||
|
||||
await entity.async_set_fan_mode('high')
|
||||
await entity.async_set_swing_mode('off')
|
||||
await entity.async_set_preset_mode('quiet') # from its own supportedModes
|
||||
|
||||
kinds = [payload[0] for _, payload in coordinator.commands]
|
||||
assert kinds == ['fan', 'swing', 'preset']
|
||||
@@ -9,7 +9,7 @@ from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import for_device, for_device_by_model
|
||||
from custom_components.localthings.registry.capabilities import airconditioner
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.entities import ClimateDesc
|
||||
from custom_components.localthings.registry.entities import ClimateDesc, SelectDesc
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
@@ -131,11 +131,12 @@ def test_climate_consumed_hrefs_declared_as_coverage():
|
||||
(as no-entity coverage caps) so they don't leak as gaps -- but produce no
|
||||
standalone entities. /temperature/current/0 and /temperatures/vs/0 are
|
||||
NOT in this list -- CURRENT_TEMPERATURE / CURRENT_TEMPERATURE_VS give
|
||||
those two real sensor entities (issue #75)."""
|
||||
those two real sensor entities (issue #75). /sensors/vs/0 is also NOT
|
||||
here -- AIR_QUALITY gives it real entity sensors."""
|
||||
reg, _ = _ac()
|
||||
for href in ('/power/0', '/power/vs/0', '/temperature/desired/0',
|
||||
'/wind/strength/vs/0', '/mode/convenient/vs/0',
|
||||
'/sensors/vs/0', '/humidity/0'):
|
||||
'/humidity/0'):
|
||||
caps = reg.capabilities.get(href)
|
||||
assert caps, href
|
||||
assert all(c.entities == () for c in caps), href
|
||||
@@ -404,4 +405,347 @@ def test_current_temperature_vs_only_binds_when_ocf_href_absent():
|
||||
def test_humidity_reads_five_percent_field_not_stuck_humidity_field():
|
||||
desc = airconditioner.HUMIDITY.entities[0]
|
||||
rep = {'x.com.samsung.da.humidity': '0', 'x.com.samsung.da.fivepercentHumidity': '42'}
|
||||
assert desc.value_fn(rep.get(desc.field)) == 42.0
|
||||
assert desc.rep_fn(rep) == 42.0
|
||||
|
||||
|
||||
def test_humidity_falls_back_to_the_plain_field_where_five_percent_is_absent():
|
||||
"""ARTIK051 boards (issue #136) have no fivepercentHumidity field at all.
|
||||
Their plain field is not stuck -- it carries a reading while Air monitoring
|
||||
is on -- so 0 means "not measuring" on both generations, not 0% humidity."""
|
||||
desc = airconditioner.HUMIDITY.entities[0]
|
||||
assert desc.rep_fn({'x.com.samsung.da.humidity': '51'}) == 51.0
|
||||
assert desc.rep_fn({'x.com.samsung.da.humidity': '0'}) is None
|
||||
assert desc.rep_fn({}) is None
|
||||
|
||||
|
||||
def test_humidity_five_percent_field_passes_a_genuine_zero_through():
|
||||
"""issue #160: fivepercentHumidity's zero-as-"not measuring" carve-out
|
||||
(added in #146 to cover ARTIK051's plain humidity field) was
|
||||
over-applied to fivepercentHumidity too, silently turning a real 0%
|
||||
reading on every other AC board into unknown. Only the humidity
|
||||
fallback field collapses 0 -- fivepercentHumidity's 0 is a real
|
||||
reading."""
|
||||
desc = airconditioner.HUMIDITY.entities[0]
|
||||
assert desc.rep_fn({'x.com.samsung.da.fivepercentHumidity': '0'}) == 0.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Wind-Free 2-in-1 (TP2X_FAC_BORA_21K, issues #150/#153): a floor-standing +
|
||||
# wall-mounted indoor unit pair sharing one outdoor unit and one local IP.
|
||||
# Reported "no climate entity is generated, only power" -- the device simply
|
||||
# fell back to 'unknown' for lack of a '_FAC_' modelNum routing token; once
|
||||
# routed, it binds against the exact same CLIMATE composite every other RAC
|
||||
# family uses, zero unbound hrefs, no new capabilities needed beyond ignoring
|
||||
# the two hrefs (/subdevices/vs/0, /runn/vs/0) unique to this board.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _ac_fac_bora():
|
||||
resources = _load_device('airconditioner_fac_bora')
|
||||
info = resources['/information/vs/0']
|
||||
reg = for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'],
|
||||
)
|
||||
return reg, resources
|
||||
|
||||
|
||||
def test_fac_bora_resolves_to_airconditioner_registry():
|
||||
reg, _ = _ac_fac_bora()
|
||||
assert reg is not None and reg.name == 'airconditioner'
|
||||
|
||||
|
||||
def test_fac_bora_no_unbound_hrefs():
|
||||
reg, resources = _ac_fac_bora()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_fac_bora_climate_entity_present():
|
||||
"""The actual reported gap: only a power switch existed before, no
|
||||
climate entity at all."""
|
||||
reg, resources = _ac_fac_bora()
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||
assert any(isinstance(item.desc, ClimateDesc) for item in bound)
|
||||
|
||||
|
||||
def test_fac_bora_subdevices_and_runningmode_are_ignored_not_guessed():
|
||||
assert '/subdevices/vs/0' in airconditioner._AC_IGNORED
|
||||
assert '/runn/vs/0' in airconditioner._AC_IGNORED
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Additive entities layered on the ARTIK051_PRAC family on top of the upstream
|
||||
# registry: beep (Volume_* option), tropical night mode (Sleep_<N> option),
|
||||
# filter usage hours + alarm threshold (filterUsage / filterDesiredUsage),
|
||||
# air-quality sensors (/sensors/vs/0 items), and software/firmware version
|
||||
# (/information/vs/0 items). Beep and tropical night use the single-token
|
||||
# option_write merge -- a full options RMW reverts on ARTIK051_PRAC (see the
|
||||
# [[samsung-ac-local-vs-cloud-control]] memory).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _beep_desc():
|
||||
return next(e for e in airconditioner.CLIMATE.entities if e.key == 'beep')
|
||||
|
||||
|
||||
def _tropical_desc():
|
||||
return next(e for e in airconditioner.CLIMATE.entities
|
||||
if e.key == 'tropical_night_mode')
|
||||
|
||||
|
||||
def test_beep_read_from_volume_token():
|
||||
"""Volume_100 (and any non-Mute) -> on; Volume_Mute -> off; no Volume_ slot
|
||||
-> None (entity won't bind via exists_fn)."""
|
||||
assert airconditioner._beep_on(
|
||||
{'x.com.samsung.da.options': ['Volume_100']}) is True
|
||||
assert airconditioner._beep_on(
|
||||
{'x.com.samsung.da.options': ['Volume_Mute']}) is False
|
||||
assert airconditioner._beep_on(
|
||||
{'x.com.samsung.da.options': ['Light_Off']}) is None
|
||||
assert airconditioner._beep_on({}) is None
|
||||
|
||||
|
||||
def test_beep_write_is_single_token_options_merge():
|
||||
"""One-element options array, not a full RMW (which reverts on
|
||||
ARTIK051_PRAC). 'On' restores the last non-Mute level so an intermediate
|
||||
setting (e.g. Volume_50) survives an off/on cycle; falls back to 100 when
|
||||
no prior level is known or the prior token is itself Mute."""
|
||||
write = _beep_desc().write_fn
|
||||
assert write('On', {}) == (
|
||||
['mode', 'vs', '0'], {'x.com.samsung.da.options': ['Volume_100']})
|
||||
assert write('On', {'x.com.samsung.da.options': ['Volume_50']}) == (
|
||||
['mode', 'vs', '0'], {'x.com.samsung.da.options': ['Volume_50']})
|
||||
assert write('On', {'x.com.samsung.da.options': ['Volume_Mute']}) == (
|
||||
['mode', 'vs', '0'], {'x.com.samsung.da.options': ['Volume_100']})
|
||||
assert write('Off', {}) == (
|
||||
['mode', 'vs', '0'], {'x.com.samsung.da.options': ['Volume_Mute']})
|
||||
assert write('Bogus', {}) is None
|
||||
|
||||
|
||||
def test_beep_absent_when_no_volume_token():
|
||||
"""TP1X_DA-AC-RAC-01011 carries no Volume_ option -- beep must not bind."""
|
||||
reg, resources = _ac_tp1x()
|
||||
state = flatten(
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
|
||||
assert 'beep' not in state
|
||||
|
||||
|
||||
def test_beep_state_on_windfree():
|
||||
"""The WindFree fixture reports Volume_100 -> beep reads True."""
|
||||
reg, resources = _ac_windfree()
|
||||
state = flatten(
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
|
||||
assert state['beep'] is True
|
||||
|
||||
|
||||
def test_tropical_night_read_from_sleep_token():
|
||||
"""Sleep_<N> -> N; absent -> None."""
|
||||
assert airconditioner._tropical_night_value(
|
||||
{'x.com.samsung.da.options': ['Sleep_0']}) == 0
|
||||
assert airconditioner._tropical_night_value(
|
||||
{'x.com.samsung.da.options': ['Sleep_16']}) == 16
|
||||
assert airconditioner._tropical_night_value(
|
||||
{'x.com.samsung.da.options': ['Volume_100']}) is None
|
||||
assert airconditioner._tropical_night_value({}) is None
|
||||
|
||||
|
||||
def test_tropical_night_write_is_single_token_options_merge():
|
||||
"""Valid 0-16 -> `['Sleep_<N>']`; out of range / non-numeric -> None (no
|
||||
write). Cloud counterpart: custom.airConditionerTropicalNightMode (0-16)."""
|
||||
write = _tropical_desc().write_fn
|
||||
assert write(0, {}) == (
|
||||
['mode', 'vs', '0'], {'x.com.samsung.da.options': ['Sleep_0']})
|
||||
assert write(16, {}) == (
|
||||
['mode', 'vs', '0'], {'x.com.samsung.da.options': ['Sleep_16']})
|
||||
assert write(17, {}) is None
|
||||
assert write(-1, {}) is None
|
||||
assert write('not-a-number', {}) is None
|
||||
# Float rounds to nearest int within range.
|
||||
assert write(5.6, {}) == (
|
||||
['mode', 'vs', '0'], {'x.com.samsung.da.options': ['Sleep_6']})
|
||||
|
||||
|
||||
def test_tropical_night_disabled_by_default():
|
||||
"""Issue #166: Sleep_0 is present in /mode/vs/0's options on every dump
|
||||
seen, including the original issue #17 dump this capability was verified
|
||||
against (see airconditioner_device.json / _ac()) -- yet the #166 reporter
|
||||
confirmed their unit (also an ARTIK051_PRAC_20K board, per its own
|
||||
/information/vs/0) has no tropical night mode feature at all. The token
|
||||
slot's presence proves nothing about the physical feature, so this can't
|
||||
be existence-gated any tighter than it already is -- registered but
|
||||
disabled by default instead, same precedent as fridge.rack_count /
|
||||
cooktop.paired_hood_model."""
|
||||
assert _tropical_desc().enabled_default is False
|
||||
|
||||
|
||||
def test_tropical_night_absent_when_no_sleep_token():
|
||||
"""TP1X_DA-AC-WAC (window AC) carries no Sleep_ option -- tropical night
|
||||
mode must not bind."""
|
||||
resources = _load_device('airconditioner_window_ac')
|
||||
info = resources['/information/vs/0']
|
||||
reg = for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
|
||||
state = flatten(
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
|
||||
assert 'tropical_night_mode' not in state
|
||||
|
||||
|
||||
def test_tropical_night_state_levels_across_fixtures():
|
||||
"""Sleep_0 / Sleep_6 / Sleep_16 surface as 0 / 6 / 16 respectively."""
|
||||
def level(name):
|
||||
res = _load_device(name)
|
||||
info = res['/information/vs/0']
|
||||
r = for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
|
||||
return flatten(discover(res, r.capabilities, r.pattern_capabilities), res).get(
|
||||
'tropical_night_mode')
|
||||
assert level('airconditioner_windfree') == 0
|
||||
assert level('airconditioner_tp1x_da_ac_rac_01011') == 6
|
||||
assert level('airconditioner_tp2x_rac_20k') == 16
|
||||
|
||||
|
||||
def test_air_filter_usage_hours_reads_raw_count():
|
||||
"""filterUsage is a lifetime hour counter (41 of 500) that resets on
|
||||
filter replacement -- total_increasing, not measurement. Unit comes from
|
||||
filterCapacityUnit via unit_fn, not a hardcoded 'h'."""
|
||||
desc = next(e for e in airconditioner.AIR_FILTER.entities
|
||||
if e.key == 'air_filter_usage_hours')
|
||||
assert desc.value_fn('41') == 41
|
||||
assert desc.value_fn(41) == 41
|
||||
assert desc.value_fn(None) is None
|
||||
assert desc.value_fn('not-a-number') is None
|
||||
assert desc.device_class == 'duration'
|
||||
assert desc.state_class == 'total_increasing'
|
||||
assert desc.unit_fn({'x.com.samsung.da.filterCapacityUnit': 'Hour'}) == 'h'
|
||||
assert desc.unit_fn({'x.com.samsung.da.filterCapacityUnit': 'Minute'}) == 'min'
|
||||
assert desc.unit_fn({}) == 'h' # static fallback when the field is absent
|
||||
|
||||
|
||||
def test_air_filter_threshold_is_writable_select():
|
||||
"""filterDesiredUsage is a locally writable option (confirmed live on
|
||||
ARTIK051_PRAC: POST 700 -> 2.04, persisted). Exposed as a Select keyed to
|
||||
the device's supportedFilterDesiredUsage enum; the write POSTs the scalar
|
||||
field back to /filter/airdustfilter/vs/0. Only binds where the enum is
|
||||
advertised -- boards without it leave this writable field unexposed rather
|
||||
than guess the valid set."""
|
||||
desc = next(e for e in airconditioner.AIR_FILTER.entities
|
||||
if e.key == 'air_filter_threshold')
|
||||
assert isinstance(desc, SelectDesc)
|
||||
assert desc.options_field == 'x.com.samsung.da.supportedFilterDesiredUsage'
|
||||
assert desc.exists_fn(
|
||||
{'x.com.samsung.da.supportedFilterDesiredUsage': ['180', '300', '500', '700']},
|
||||
{}) is True
|
||||
assert desc.exists_fn({}, {}) is False
|
||||
# Current value is stringified for option matching.
|
||||
assert desc.value_fn('500') == '500'
|
||||
assert desc.value_fn(500) == '500'
|
||||
assert desc.value_fn(None) is None
|
||||
# Write POSTs the selected option as the scalar field.
|
||||
assert desc.write_fn('700', {}) == (
|
||||
['filter', 'airdustfilter', 'vs', '0'],
|
||||
{'x.com.samsung.da.filterDesiredUsage': '700'})
|
||||
|
||||
|
||||
def test_air_filter_threshold_absent_without_supported_enum():
|
||||
"""WindFree (ARTIK051_PRAC) advertises no supportedFilterDesiredUsage, so
|
||||
the writable threshold Select must not bind there -- even though the
|
||||
scalar field is present and writable. Don't expose a control whose valid
|
||||
options aren't known."""
|
||||
reg, resources = _ac_windfree()
|
||||
state = flatten(
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
|
||||
assert 'air_filter_threshold' not in state
|
||||
assert state['air_filter_usage_hours'] == 41
|
||||
assert state['air_filter_usage'] == 8 # 41/500 -> 8%
|
||||
|
||||
|
||||
def test_air_filter_threshold_binds_on_enum_board():
|
||||
"""tp1x_rac advertises supportedFilterDesiredUsage -> threshold Select
|
||||
binds, current value read from filterDesiredUsage."""
|
||||
reg, resources = _resolve('airconditioner_tp1x_rac')
|
||||
state = flatten(
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
|
||||
assert state['air_filter_threshold'] == '500'
|
||||
|
||||
|
||||
def test_air_quality_sensors_from_sensors_vs_items():
|
||||
"""/sensors/vs/0 items[] surface as diagnostic scalars (no unit advertised
|
||||
on the resource, so no device_class until a populated reading + unit is
|
||||
observed -- the 'don't guess' rule). CleanLevel is corroborated as numeric
|
||||
by a top-level cleanLevel scalar, so it's an int measurement; the others
|
||||
are string diagnostics. Dust/FineDust/SuperFineDust carry a 2-element
|
||||
array whose second element is unconfirmed -- v[0] is taken as the reading
|
||||
(see _sensor_item_value)."""
|
||||
reg, resources = _ac_windfree()
|
||||
state = flatten(
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
|
||||
assert state['clean_level'] == 0 # numeric (int), corroborated
|
||||
for key in ('odor', 'dust', 'fine_dust', 'super_fine_dust'):
|
||||
assert state[key] == '0' # string diagnostic
|
||||
# tp1x_da_ac_rac_01011 is the only fixture with a non-zero air-quality
|
||||
# reading -- the one that catches a value_fn regression.
|
||||
reg2, resources2 = _ac_tp1x()
|
||||
state2 = flatten(
|
||||
discover(resources2, reg2.capabilities, reg2.pattern_capabilities), resources2)
|
||||
assert state2['clean_level'] == 1
|
||||
|
||||
|
||||
def test_air_quality_disabled_by_default():
|
||||
"""Issue #166 (ARxxTXFCAWKNEU, board ARTIK051_PRAC_20K): /sensors/vs/0
|
||||
lists all five item types with permanent zero values on both its units --
|
||||
the exact same shape as the WindFree/issue #17 dumps this capability was
|
||||
first verified against -- yet the reporter confirmed none of these
|
||||
sensors are physically present on their model.
|
||||
|
||||
A tighter exists_fn (requiring a corroborating top-level
|
||||
x.com.samsung.da.cleanLevel scalar) was tried and reverted: it looked
|
||||
like a real signal against this repo's AC fixtures, but
|
||||
air_purifier_device.json, air_purifier_vtww_device.json, and
|
||||
range_hood_device.json all carry genuinely populated Dust/FineDust/
|
||||
SuperFineDust readings with no such scalar, so requiring it would
|
||||
silently drop real readings on hardware this repo hasn't seen yet on an
|
||||
AC. These stay bound whenever the item type is listed (see
|
||||
_has_sensor_type) and disabled by default instead, same precedent as
|
||||
fridge.rack_count / cooktop.paired_hood_model / tropical_night_mode --
|
||||
units that do have the sensor can enable it themselves."""
|
||||
for key in ('clean_level', 'odor', 'dust', 'fine_dust', 'super_fine_dust'):
|
||||
desc = next(e for e in airconditioner.AIR_QUALITY.entities if e.key == key)
|
||||
assert desc.enabled_default is False, key
|
||||
|
||||
|
||||
def test_air_quality_absent_when_no_sensor_items():
|
||||
"""A board whose /sensors/vs/0 carries an empty items[] (the cool-only
|
||||
RAC variant) binds no air-quality entities -- exists_fn gates each on its
|
||||
item type, not merely on the href being present."""
|
||||
reg, resources = _resolve('airconditioner_tp1x_rac_coolonly')
|
||||
assert '/sensors/vs/0' in resources # the href is there, just empty
|
||||
state = flatten(
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
|
||||
for key in ('clean_level', 'odor', 'dust', 'fine_dust', 'super_fine_dust'):
|
||||
assert key not in state, key
|
||||
|
||||
|
||||
def test_sensor_item_value_picks_first_value():
|
||||
"""_sensor_item_value returns the first element of the value list, as a
|
||||
string; None when the item is absent or its value is empty."""
|
||||
items = [
|
||||
{'x.com.samsung.da.type': 'Dust', 'x.com.samsung.da.value': ['0', '0']},
|
||||
{'x.com.samsung.da.type': 'Odor', 'x.com.samsung.da.value': []},
|
||||
]
|
||||
assert airconditioner._sensor_item_value(items, 'Dust') == '0'
|
||||
assert airconditioner._sensor_item_value(items, 'Odor') is None
|
||||
assert airconditioner._sensor_item_value(items, 'Missing') is None
|
||||
assert airconditioner._sensor_item_value(None, 'Dust') is None
|
||||
|
||||
|
||||
def test_beep_and_tropical_night_stay_off_legacy_krac_board():
|
||||
"""ARTIK051_KRAC_18K (issue #136) reports both a Volume_ and a Sleep_
|
||||
option token, but they're already modeled as buzzer_volume/good_sleep
|
||||
(see airconditioner.CLIMATE) -- beep/tropical_night_mode must not also
|
||||
bind there, or the same options[] slot would surface as two entities."""
|
||||
reg, resources = _resolve('airconditioner_artik051_krac_18k')
|
||||
state = flatten(
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
|
||||
assert 'beep' not in state
|
||||
assert 'tropical_night_mode' not in state
|
||||
assert state['buzzer_volume'] == 100.0
|
||||
assert state['good_sleep'] == 0.0
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"""TP1X_DA-AC-RAC-01001_0000 fan-strength codes (model AR07C9150HZN, issue
|
||||
#155).
|
||||
|
||||
Its /wind/strength/vs/0 reports supportedModes "0"/"31"/"32"/"33"/"34"/"35"
|
||||
instead of the "0"-"4" scale climate.py's _DEVICE_TO_FAN was built from
|
||||
(every other AC fixture in this repo uses "0"-"4", some with a 6th "5" --
|
||||
see airconditioner_window_ac_device.json). Only "0" matched _DEVICE_TO_FAN,
|
||||
so fan_modes silently dropped every speed but Auto. The fix reads the
|
||||
device's own modesName labels (parallel-indexed with supportedModes) for
|
||||
any code _DEVICE_TO_FAN doesn't already cover, instead of hardcoding a
|
||||
second numeric scale.
|
||||
"""
|
||||
from custom_components.localthings.climate import (
|
||||
LocalThingsClimate, _DEVICE_TO_FAN, _wind_strength_label,
|
||||
)
|
||||
from custom_components.localthings.registry import by_type
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.entities import ClimateDesc
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
FIXTURE = 'airconditioner_tp1x_rac_01001'
|
||||
|
||||
|
||||
class _FakeCoordinator:
|
||||
device_serial = 'TEST-RAC-01001-SERIAL'
|
||||
device_info = {}
|
||||
data = {}
|
||||
|
||||
def __init__(self, resources):
|
||||
self.last_resources = resources
|
||||
self.commands = []
|
||||
|
||||
def resource(self, href):
|
||||
return self.last_resources.get(href, {})
|
||||
|
||||
async def async_send_command(self, bound, payload):
|
||||
self.commands.append((bound, payload))
|
||||
|
||||
|
||||
def _climate(resources, coordinator=None):
|
||||
info = resources['/information/vs/0']
|
||||
reg = by_type.for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||
climate_bound = next(item for item in bound if isinstance(item.desc, ClimateDesc))
|
||||
return LocalThingsClimate(coordinator or _FakeCoordinator(resources), climate_bound)
|
||||
|
||||
|
||||
def test_wind_strength_label_reads_the_devices_own_modes_name():
|
||||
rep = {
|
||||
'x.com.samsung.da.supportedModes': ['0', '31', '32', '33', '34', '35'],
|
||||
'x.com.samsung.da.modesName': ['Auto', '1', '2', '3', '4', 'MAX'],
|
||||
}
|
||||
assert _wind_strength_label('32', rep) == '2'
|
||||
assert _wind_strength_label('35', rep) == 'max'
|
||||
|
||||
|
||||
def test_wind_strength_label_falls_back_to_raw_code_when_names_absent():
|
||||
assert _wind_strength_label('32', {}) == '32'
|
||||
|
||||
|
||||
def test_fan_modes_include_every_supported_speed_not_just_auto():
|
||||
"""Before the fix, only '0' matched _DEVICE_TO_FAN and fan_modes was
|
||||
['auto'] -- exactly the reported symptom."""
|
||||
resources = _load_device(FIXTURE)
|
||||
entity = _climate(resources)
|
||||
assert entity.fan_modes == ['auto', '1', '2', '3', '4', 'max']
|
||||
|
||||
|
||||
def test_fan_mode_reads_the_current_dynamic_code():
|
||||
"""Fixture's /wind/strength/vs/0 modes is '32' -> modesName '2'."""
|
||||
resources = _load_device(FIXTURE)
|
||||
entity = _climate(resources)
|
||||
assert entity.fan_mode == '2'
|
||||
|
||||
|
||||
def test_standard_scale_codes_still_use_device_to_fan():
|
||||
"""A code _DEVICE_TO_FAN already covers keeps its existing friendly
|
||||
label rather than falling through to the device's own (blunter) one --
|
||||
no regression for boards using the standard "0"-"4" scale."""
|
||||
resources = _load_device(FIXTURE)
|
||||
resources['/wind/strength/vs/0']['x.com.samsung.da.modes'] = '0'
|
||||
entity = _climate(resources)
|
||||
assert entity.fan_mode == _DEVICE_TO_FAN['0'] == 'auto'
|
||||
|
||||
|
||||
async def test_set_fan_mode_resolves_a_dynamic_label_back_to_its_code():
|
||||
resources = _load_device(FIXTURE)
|
||||
coordinator = _FakeCoordinator(resources)
|
||||
entity = _climate(resources, coordinator)
|
||||
|
||||
await entity.async_set_fan_mode('max')
|
||||
|
||||
assert coordinator.commands[-1][1] == ('fan', '35')
|
||||
|
||||
|
||||
async def test_set_fan_mode_still_resolves_standard_scale_labels():
|
||||
resources = _load_device(FIXTURE)
|
||||
coordinator = _FakeCoordinator(resources)
|
||||
entity = _climate(resources, coordinator)
|
||||
|
||||
await entity.async_set_fan_mode('auto')
|
||||
|
||||
assert coordinator.commands[-1][1] == ('fan', '0')
|
||||
|
||||
|
||||
async def test_set_fan_mode_does_not_misroute_when_static_map_and_live_codes_collide():
|
||||
"""A board can use non-standard codes ('31'-'33') while modesName still
|
||||
spells a standard-looking label ('Low'/'High') that _FAN_TO_DEVICE's
|
||||
static reverse map also happens to have an entry for ('1'/'3') -- but
|
||||
that entry is for a *different* code this unit never advertises at all.
|
||||
Resolving the static hit without checking it against this unit's own
|
||||
supportedModes would silently write a code the device doesn't have.
|
||||
"""
|
||||
resources = _load_device(FIXTURE)
|
||||
resources['/wind/strength/vs/0'] = {
|
||||
'x.com.samsung.da.modes': '0',
|
||||
'x.com.samsung.da.supportedModes': ['0', '31', '32', '33'],
|
||||
'x.com.samsung.da.modesName': ['Auto', 'Low', 'High', 'Turbo'],
|
||||
}
|
||||
coordinator = _FakeCoordinator(resources)
|
||||
entity = _climate(resources, coordinator)
|
||||
|
||||
assert entity.fan_modes == ['auto', 'low', 'high', 'turbo']
|
||||
|
||||
await entity.async_set_fan_mode('high')
|
||||
assert coordinator.commands[-1][1] == ('fan', '32')
|
||||
|
||||
await entity.async_set_fan_mode('low')
|
||||
assert coordinator.commands[-1][1] == ('fan', '31')
|
||||
|
||||
await entity.async_set_fan_mode('turbo')
|
||||
assert coordinator.commands[-1][1] == ('fan', '33')
|
||||
@@ -7,8 +7,8 @@ testable directly.
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from custom_components.localthings.climate import (
|
||||
_AI_COMFORT_MODE, _DEVICE_TO_HVAC, _HVAC_TO_DEVICE, PRESET_AI_COMFORT,
|
||||
_preset_to_ha,
|
||||
_AI_COMFORT_MODE, _DEVICE_TO_HVAC, _HVAC_TO_DEVICE,
|
||||
PRESET_AI_COMFORT, _preset_to_ha,
|
||||
)
|
||||
|
||||
|
||||
@@ -74,3 +74,45 @@ def test_preset_to_ha_lowercases_other_codes():
|
||||
assert _preset_to_ha('Sleep') == 'sleep'
|
||||
assert _preset_to_ha('NanoSleep') == 'nanosleep'
|
||||
assert _preset_to_ha('MotionIndirect') == 'motionindirect'
|
||||
|
||||
|
||||
def test_fac_bora_wind_strength_codes_fit_the_standard_scale():
|
||||
"""TP2X_FAC_BORA_21K's (issues #150/#153) /wind/strength/vs/0 codes
|
||||
(0/2/3/4, skipping 1/'low') and modesName (Auto/Mid/High/Turbo) already
|
||||
match _DEVICE_TO_FAN's own mapping exactly -- no dynamic modesName
|
||||
fallback needed for this particular board, unlike issue #155's
|
||||
TP1X_DA-AC-RAC-01001_0000.
|
||||
|
||||
Asserts the live climate entity's actual fan_modes/fan_mode output
|
||||
(not just that the module constant _DEVICE_TO_FAN happens to have
|
||||
these keys) -- a bare `code in _DEVICE_TO_FAN` check would still pass
|
||||
even if fan_modes/fan_mode were completely broken, since it never
|
||||
touches the entity at all.
|
||||
"""
|
||||
from custom_components.localthings.climate import LocalThingsClimate
|
||||
from custom_components.localthings.registry import by_type
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.entities import ClimateDesc
|
||||
from tests.conftest import _load_device
|
||||
|
||||
class _FakeCoordinator:
|
||||
device_serial = 'TEST-FAC-BORA-SERIAL'
|
||||
device_info = {}
|
||||
data = {}
|
||||
|
||||
def __init__(self, resources):
|
||||
self.last_resources = resources
|
||||
|
||||
def resource(self, href):
|
||||
return self.last_resources.get(href, {})
|
||||
|
||||
resources = _load_device('airconditioner_fac_bora')
|
||||
info = resources['/information/vs/0']
|
||||
reg = by_type.for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||
climate_bound = next(item for item in bound if isinstance(item.desc, ClimateDesc))
|
||||
entity = LocalThingsClimate(_FakeCoordinator(resources), climate_bound)
|
||||
|
||||
assert entity.fan_modes == ['auto', 'medium', 'high', 'turbo']
|
||||
assert entity.fan_mode == 'auto'
|
||||
|
||||
@@ -184,6 +184,40 @@ class TestWmSetinfoFlags:
|
||||
},
|
||||
]) == 'LE'
|
||||
|
||||
def test_off_suffixed_placeholder_codes_filtered(self):
|
||||
"""issue #166: Samsung pre-populates /alarms/vs/0 with one row per
|
||||
supported alarm *type*, each carrying a '<Name>_OFF' placeholder
|
||||
code (no 'Deleted' state at all) when that alarm isn't firing --
|
||||
confirmed not just for 'ErrorCode_OFF' but also 'FilterAlarm_OFF'
|
||||
on the same dump. These were previously shown as if they were
|
||||
active alarms."""
|
||||
assert common._active_alarm_codes([
|
||||
{
|
||||
'x.com.samsung.da.code': 'ErrorCode_OFF',
|
||||
'x.com.samsung.da.triggeredTime': '2026-07-28T12:59:22',
|
||||
},
|
||||
{
|
||||
'x.com.samsung.da.code': 'FilterAlarm_OFF',
|
||||
'x.com.samsung.da.triggeredTime': '2026-07-28T12:59:22',
|
||||
},
|
||||
]) == 'none'
|
||||
|
||||
def test_real_filter_alarm_not_off_suffixed_still_shown(self):
|
||||
"""issue #166's actual live filter alert: code 'FilterAlarm' (no
|
||||
'_OFF' suffix), state 'Created' -- distinct from the 'FilterAlarm_OFF'
|
||||
placeholder and must still surface."""
|
||||
assert common._active_alarm_codes([
|
||||
{
|
||||
'x.com.samsung.da.code': 'ErrorCode_OFF',
|
||||
'x.com.samsung.da.triggeredTime': '2026-07-28T12:59:22',
|
||||
},
|
||||
{
|
||||
'x.com.samsung.da.code': 'FilterAlarm',
|
||||
'x.com.samsung.da.state': 'Created',
|
||||
'x.com.samsung.da.triggeredTime': '2026-07-28T12:59:22',
|
||||
},
|
||||
]) == 'FilterAlarm'
|
||||
|
||||
|
||||
class TestKidsLockFallback:
|
||||
def test_generic_read_write(self):
|
||||
|
||||
@@ -209,3 +209,19 @@ def test_discover_rt_filter_gates_binding():
|
||||
resources = {'/mode/vs/0': {'rt': ['x.com.samsung.da.mode'], 'x.com.samsung.da.mode': 'Bake'}}
|
||||
bound = discover(resources, {'/mode/vs/0': [oven_mode_cap]})
|
||||
assert bound == []
|
||||
|
||||
|
||||
def test_discover_tier_log_fires_for_no_entity_coverage_cap():
|
||||
"""A coverage-only Capability (entities=(), used to mark a href as
|
||||
handled by e.g. a composite climate entity rather than its own HA
|
||||
entity) produces zero BoundEntity rows -- `bound` alone can't tell a
|
||||
caller its poll_tier. tier_log must still report it, or a consumer
|
||||
(the coordinator's hot/warm href lists) silently treats it as 'cold'
|
||||
regardless of what poll_tier the capability actually declares."""
|
||||
coverage_cap = Capability(href='/wind/strength/vs/0', poll_tier='warm')
|
||||
resources = {'/wind/strength/vs/0': {'x.com.samsung.da.modes': '0'}}
|
||||
seen = []
|
||||
bound = discover(resources, {coverage_cap.href: [coverage_cap]},
|
||||
tier_log=lambda href, tier: seen.append((href, tier)))
|
||||
assert bound == []
|
||||
assert seen == [('/wind/strength/vs/0', 'warm')]
|
||||
|
||||
@@ -191,6 +191,27 @@ def test_registry_reproduces_golden_state_keys_for_tp1x_ref_21k_us():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_tp1x_ref_21k_eu():
|
||||
"""TP1X_REF_21K, EU region variant (issue #165) -- self-reports
|
||||
oneUiVersion "7.0 Refrigerator" like the US variant, but additionally
|
||||
carries /rm/control/vs/0 (a bare resource-monitoring poll-interval
|
||||
config, added to the global ignore list) that the US dump doesn't
|
||||
report at all. Door sensors (the reporter's actual ask) were already
|
||||
covered by fridge.DOOR_GENERIC/DOORS_FALLBACK -- this href was the only
|
||||
gap keeping the coverage repair open."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('refrigerator_tp1x_ref_21k_eu')
|
||||
golden = json.loads(
|
||||
(GOLDEN / 'refrigerator_tp1x_ref_21k_eu.json').read_text()
|
||||
)
|
||||
state_keys = _new_state_keys('refrigerator_tp1x_ref_21k_eu', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_range_hood():
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('range_hood')
|
||||
@@ -457,6 +478,12 @@ def test_registry_reproduces_golden_state_keys_for_induction_cooktop():
|
||||
resources = _load_device('induction_cooktop')
|
||||
golden = json.loads((GOLDEN / 'induction_cooktop.json').read_text())
|
||||
state_keys = _new_state_keys('induction_cooktop', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_range_no_info():
|
||||
"""NE63B8411SS (issue #74) -- reports no oneUiVersion *and* no
|
||||
@@ -544,6 +571,23 @@ def test_registry_reproduces_golden_state_keys_for_range_ne8300d():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_range_ne63a6511():
|
||||
"""NE63A6511SS/AA (issue #138) -- reports no /information/vs/0 at all,
|
||||
same shape as issue #74's NE63B8411SS; resolved via the same
|
||||
'Bake'-in-supportedModes + /cooktopmonitoring/vs/0 signature in
|
||||
for_device_by_resources. Binds cleanly against the existing range
|
||||
registry with zero unbound hrefs."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('range_ne63a6511')
|
||||
golden = json.loads((GOLDEN / 'range_ne63a6511.json').read_text())
|
||||
state_keys = _new_state_keys('range_ne63a6511', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_airconditioner_ara_ww_tp1_22():
|
||||
"""ARA-WW-TP1-22-COMMON wall-mount RACs (model AR10/13/18BYEAAWKNME,
|
||||
issues #115/#116/#117/#120) report no oneUiVersion and no
|
||||
@@ -630,6 +674,22 @@ def test_registry_reproduces_golden_state_keys_for_microwave_me7500d():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_microwave_me7500d_lamp_high():
|
||||
"""Same TP1X_DA-KS-MICROWAVE-01051/ME7500D board as microwave_me7500d
|
||||
above, but this live capture (issue #152) is the first to report a
|
||||
non-Off Lamp token ('Lamp_High'). Locks in that the lamp switch reads
|
||||
it as on rather than the previously-hardcoded 'On'-only comparison."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('microwave_me7500d_lamp_high')
|
||||
golden = json.loads((GOLDEN / 'microwave_me7500d_lamp_high.json').read_text())
|
||||
state_keys = _new_state_keys('microwave_me7500d_lamp_high', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_air_purifier_tp1x_da_ac_air():
|
||||
"""TP1X_DA-AC-AIR-01031_0000 (issue #130) self-reports oneUiVersion
|
||||
'7.0 Air purifier' and resolves via for_device() onto the existing
|
||||
@@ -671,6 +731,124 @@ def test_registry_reproduces_golden_state_keys_for_vacuum_station():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_artik051_krac_18k():
|
||||
"""ARTIK051_KRAC_18K (issue #136) -- reports no oneUiVersion, and its
|
||||
'_KRAC_' token was invisible to for_device_by_model's '_RAC_' check (the
|
||||
'K' sits between the underscore and 'RAC'), so it fell back to 'unknown'
|
||||
and exposed nothing but power. Same ARTIK051 board generation as the
|
||||
'_TVTL_' air purifier: no /wind/* resources at all (fan and vane live in
|
||||
/airflow/vs/0), no /mode/convenient/vs/0 (the preset is a Comode_* token),
|
||||
and several settings carried as /mode/vs/0 options[] tokens."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('airconditioner_artik051_krac_18k')
|
||||
golden = json.loads(
|
||||
(GOLDEN / 'airconditioner_artik051_krac_18k.json').read_text()
|
||||
)
|
||||
state_keys = _new_state_keys('airconditioner_artik051_krac_18k', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_airconditioner_tp1x_rac_01001():
|
||||
"""TP1X_DA-AC-RAC-01001_0000 (model AR07C9150HZN, issue #155) -- binds
|
||||
cleanly against the existing airconditioner registry with zero unbound
|
||||
hrefs (the registry/discovery side was never the gap here). Its
|
||||
/wind/strength/vs/0 reports supportedModes "0"/"31"-"35" instead of the
|
||||
"0"-"4" scale climate.py's _DEVICE_TO_FAN was built from, which silently
|
||||
dropped every fan speed but Auto -- see
|
||||
test_airconditioner_tp1x_rac_01001_fan.py for the climate-level fix."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('airconditioner_tp1x_rac_01001')
|
||||
golden = json.loads(
|
||||
(GOLDEN / 'airconditioner_tp1x_rac_01001.json').read_text()
|
||||
)
|
||||
state_keys = _new_state_keys('airconditioner_tp1x_rac_01001', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_air_dresser():
|
||||
"""DA_DF_A51_20_COMMON AirDresser (model DF8600T, issue #162) -- reports
|
||||
no oneUiVersion; resolved via the '_DF_' modelNum token fallback. Has no
|
||||
/wm/editcourse/vs/0 at all, so the course select's option list comes
|
||||
entirely from laundry.cycle_options' supportedOptions fallback. Binds
|
||||
cleanly against the new air_dresser registry with zero unbound hrefs."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('air_dresser')
|
||||
golden = json.loads((GOLDEN / 'air_dresser.json').read_text())
|
||||
state_keys = _new_state_keys('air_dresser', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_air_dresser_tp2_20():
|
||||
"""DA_DF_TP2_20_COMMON AirDresser (model DF9500A, issue #157) -- a
|
||||
different board generation than issue #162's DA_DF_A51_20_COMMON, also
|
||||
routed via the '_DF_' modelNum fallback into the same air_dresser
|
||||
registry. Unlike #162's board, this one populates /wm/editcourse/vs/0's
|
||||
editCourseList directly (no supportedOptions fallback needed) and
|
||||
reports two AirDresser-specific resources #162 doesn't have:
|
||||
/st/airdressercourse/vs/0 (course table id, ignored.py) and
|
||||
/airdresseroption/sanitize/vs/0 (air_dresser.AIR_DRESSER_SANITIZE).
|
||||
Binds cleanly with zero unbound hrefs."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('air_dresser_tp2_20')
|
||||
golden = json.loads((GOLDEN / 'air_dresser_tp2_20.json').read_text())
|
||||
state_keys = _new_state_keys('air_dresser_tp2_20', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_air_purifier_vtww():
|
||||
"""A-VTWW-TP2-21-COMMON BESPOKE Cube Air (issue #151) -- reports no
|
||||
oneUiVersion; resolved via the '-VTWW-' modelNum token fallback into
|
||||
the existing air_purifier registry. Its fan lives on
|
||||
/wind/strength/vs/0 (air_purifier.WIND_STRENGTH_FAN) rather than the
|
||||
/mode/vs/0 FAN the other two board generations in this registry use.
|
||||
Binds cleanly with zero unbound hrefs."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('air_purifier_vtww')
|
||||
golden = json.loads((GOLDEN / 'air_purifier_vtww.json').read_text())
|
||||
state_keys = _new_state_keys('air_purifier_vtww', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_airconditioner_fac_bora():
|
||||
"""TP2X_FAC_BORA_21K Wind-Free 2-in-1 (floor-standing + wall-mounted
|
||||
indoor units sharing one outdoor unit and one local IP, issues
|
||||
#150/#153) -- reports no oneUiVersion; resolved via the '_FAC_'
|
||||
modelNum fallback. Binds cleanly against the existing airconditioner
|
||||
registry (including a real climate entity, the actual reported gap)
|
||||
with zero unbound hrefs -- /subdevices/vs/0 and /runn/vs/0 are the only
|
||||
hrefs this board reports that no other AC family does, both added to
|
||||
_AC_IGNORED as undocumented/not-locally-actionable."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('airconditioner_fac_bora')
|
||||
golden = json.loads((GOLDEN / 'airconditioner_fac_bora.json').read_text())
|
||||
state_keys = _new_state_keys('airconditioner_fac_bora', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_resources_from_batch_preferred_over_flat():
|
||||
from tests.conftest import _resources_from_dump
|
||||
dump = {
|
||||
|
||||
@@ -120,9 +120,23 @@ def test_power_level_handles_missing_value():
|
||||
|
||||
def test_microwave_mode_options_nonempty():
|
||||
desc = microwave.MICROWAVE_MODE.entities[0]
|
||||
assert len(desc.options) > 0
|
||||
assert 'MicroWave' in desc.options
|
||||
assert 'AirFryer' in desc.options # distinct spelling from oven.py's 'AirFry'
|
||||
assert callable(desc.options)
|
||||
options = desc.options({})
|
||||
assert len(options) > 0
|
||||
assert 'MicroWave' in options
|
||||
assert 'AirFryer' in options # distinct spelling from oven.py's 'AirFry'
|
||||
|
||||
|
||||
def test_microwave_mode_options_reads_live_supported_modes():
|
||||
"""issue #152's ME7500D reports only 4 of the 11 union-of-all-dumps
|
||||
_MICROWAVE_MODES -- the live supportedModes list is used verbatim when
|
||||
present, same live-first pattern as oven._oven_mode_options, instead of
|
||||
offering users modes their own unit doesn't have."""
|
||||
desc = microwave.MICROWAVE_MODE.entities[0]
|
||||
resources = {'/mode/vs/0': {
|
||||
'x.com.samsung.da.supportedModes': ['NoOperation', 'MicroWave', 'Autocook', 'KeepWarm'],
|
||||
}}
|
||||
assert desc.options(resources) == ['NoOperation', 'MicroWave', 'Autocook', 'KeepWarm']
|
||||
|
||||
|
||||
def test_microwave_mode_write_round_trips():
|
||||
@@ -137,6 +151,19 @@ def test_microwave_mode_rejects_unknown():
|
||||
assert desc.write_fn('SpaghettiMode', {}) is None
|
||||
|
||||
|
||||
def test_microwave_mode_write_validates_against_live_supported_modes():
|
||||
"""A device reporting its own supportedModes is validated against that
|
||||
list, not the static union-of-all-dumps fallback -- 'AirFryer' is a
|
||||
valid _MICROWAVE_MODES entry but must still be rejected for a unit
|
||||
whose own supportedModes doesn't include it."""
|
||||
desc = microwave.MICROWAVE_MODE.entities[0]
|
||||
rep = {'x.com.samsung.da.supportedModes': ['NoOperation', 'MicroWave', 'Autocook', 'KeepWarm']}
|
||||
path, body = desc.write_fn('MicroWave', rep)
|
||||
assert path == ['mode', 'vs', '0']
|
||||
assert body['x.com.samsung.da.modes'] == ['MicroWave']
|
||||
assert desc.write_fn('AirFryer', rep) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MICROWAVE_MODE — lamp/sound options-array writes
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -165,13 +192,28 @@ def test_lamp_gated_present_when_lamp_option_reported():
|
||||
|
||||
|
||||
def test_lamp_write_is_single_token():
|
||||
"""issue #152: the device has never been observed accepting 'On' --
|
||||
only 'High'/'Off' -- so the switch's "on" write uses 'High'."""
|
||||
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
|
||||
rep = {'x.com.samsung.da.options': ['Lamp_Off']}
|
||||
path, body = desc.write_fn('On', rep)
|
||||
assert path == ['mode', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.options': ['Lamp_On']}
|
||||
assert body == {'x.com.samsung.da.options': ['Lamp_High']}
|
||||
|
||||
|
||||
def test_lamp_write_requires_existing_options():
|
||||
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
|
||||
assert desc.write_fn('On', {}) is None
|
||||
|
||||
|
||||
def test_lamp_reads_off_as_false():
|
||||
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
|
||||
assert desc.value_fn(['Lamp_Off']) is False
|
||||
|
||||
|
||||
def test_lamp_reads_any_non_off_level_as_true():
|
||||
"""issue #152's ME7500D reports 'Lamp_High', not the binary 'Lamp_On'
|
||||
#137's dump implied -- any non-Off/non-absent value must read as on, not
|
||||
just a literal 'On'."""
|
||||
desc = next(e for e in microwave.MICROWAVE_MODE.entities if e.key == 'lamp')
|
||||
assert desc.value_fn(['Lamp_High']) is True
|
||||
|
||||
@@ -120,12 +120,29 @@ def test_oven_setpoint_native_bounds_track_live_unit():
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_oven_mode_options_nonempty():
|
||||
assert len(oven.OVEN_MODE.entities[0].options) > 0
|
||||
desc = oven.OVEN_MODE.entities[0]
|
||||
assert callable(desc.options)
|
||||
assert len(desc.options({})) > 0
|
||||
|
||||
|
||||
def test_oven_mode_options_falls_back_when_no_live_supported_modes():
|
||||
desc = oven.OVEN_MODE.entities[0]
|
||||
assert desc.options({}) == list(oven._OVEN_MODES)
|
||||
|
||||
|
||||
def test_oven_mode_options_reads_live_supported_modes():
|
||||
"""issue #138: the device's own supportedModes list is used verbatim
|
||||
when present, instead of the static _OVEN_MODES guess."""
|
||||
desc = oven.OVEN_MODE.entities[0]
|
||||
resources = {'/mode/vs/0': {
|
||||
'x.com.samsung.da.supportedModes': ['Bake', 'AirFryer', 'SelfClean'],
|
||||
}}
|
||||
assert desc.options(resources) == ['Bake', 'AirFryer', 'SelfClean']
|
||||
|
||||
|
||||
def test_oven_mode_write_round_trips():
|
||||
desc = oven.OVEN_MODE.entities[0]
|
||||
valid_mode = desc.options[1] # e.g. 'Bake'
|
||||
valid_mode = desc.options({})[1] # e.g. 'Bake'
|
||||
path, body = desc.write_fn(valid_mode, {})
|
||||
assert path == ['mode', 'vs', '0']
|
||||
assert body['x.com.samsung.da.modes'] == [valid_mode]
|
||||
@@ -136,6 +153,19 @@ def test_oven_mode_rejects_unknown():
|
||||
assert desc.write_fn('SpaghettiMode', {}) is None
|
||||
|
||||
|
||||
def test_oven_mode_write_validates_against_live_supported_modes():
|
||||
"""A device reporting its own supportedModes is validated against that
|
||||
list, not the static fallback -- issue #138's AirFryer/SelfClean/etc.
|
||||
are accepted, and a mode outside the device's own list is rejected even
|
||||
if some other model's static guess would have allowed it."""
|
||||
desc = oven.OVEN_MODE.entities[0]
|
||||
rep = {'x.com.samsung.da.supportedModes': ['Bake', 'AirFryer', 'SelfClean']}
|
||||
path, body = desc.write_fn('AirFryer', rep)
|
||||
assert path == ['mode', 'vs', '0']
|
||||
assert body['x.com.samsung.da.modes'] == ['AirFryer']
|
||||
assert desc.write_fn('FrozenPizzaPlus', rep) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OVEN_MODE options-array writes (lamp, sound, fast_preheat, natural_steam).
|
||||
# Confirmed on real hardware (issue #54): a write only needs to carry the
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Tests for the NE63A6511SS/AA range (issue #138) -- same no-/information/vs/0,
|
||||
no-burner-status shape as issue #74's NE63B8411SS, confirming the existing
|
||||
range_no_info detection path still resolves this model with zero unbound
|
||||
hrefs, plus oven.OVEN_MODE reading this dump's ConvectionRoast/KeepWarm/
|
||||
BreadProof/AirFryer/Dehydrate/SelfClean/SteamClean modes live from its own
|
||||
/mode/vs/0 supportedModes rather than needing them hardcoded."""
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import for_device_by_resources
|
||||
from custom_components.localthings.registry.capabilities import oven
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _range():
|
||||
resources = _load_device('range_ne63a6511')
|
||||
reg = for_device_by_resources(resources)
|
||||
return reg, resources
|
||||
|
||||
|
||||
def _state():
|
||||
reg, resources = _range()
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||
return flatten(bound, resources)
|
||||
|
||||
|
||||
def test_resolves_to_range_registry():
|
||||
reg, _ = _range()
|
||||
assert reg is not None and reg.name == 'range'
|
||||
|
||||
|
||||
def test_no_unbound_hrefs():
|
||||
reg, resources = _range()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_expected_entities_present():
|
||||
state = _state()
|
||||
for key in (
|
||||
'power_switch', 'oven_setpoint', 'current_temp_c', 'oven_mode',
|
||||
'machine_state', 'door_open', 'cloud_connected', 'child_lock',
|
||||
'cooktop_running_state', 'warming_center_state',
|
||||
):
|
||||
assert key in state, key
|
||||
|
||||
|
||||
def test_oven_mode_accepts_this_devices_supported_modes():
|
||||
"""This dump's /mode/vs/0 supportedModes reports ConvectionRoast,
|
||||
KeepWarm, BreadProof, AirFryer, Dehydrate, SelfClean, and SteamClean --
|
||||
none of which were in the static oven._OVEN_MODES fallback, so a
|
||||
hardcoded write-validation list would've silently rejected them even
|
||||
though the device itself advertises them. write_fn reads supportedModes
|
||||
off the live rep (the same one it's called with in production -- see
|
||||
coordinator.py's async_send_command), so this passes without needing
|
||||
those modes added to any Python list."""
|
||||
_, resources = _range()
|
||||
live_rep = resources['/mode/vs/0']
|
||||
desc = oven.OVEN_MODE.entities[0]
|
||||
assert desc.options(resources) == live_rep['x.com.samsung.da.supportedModes']
|
||||
for mode in (
|
||||
'ConvectionRoast', 'KeepWarm', 'BreadProof', 'AirFryer',
|
||||
'Dehydrate', 'SelfClean', 'SteamClean',
|
||||
):
|
||||
path, body = desc.write_fn(mode, live_rep)
|
||||
assert path == ['mode', 'vs', '0']
|
||||
assert body['x.com.samsung.da.modes'] == [mode]
|
||||
Reference in New Issue
Block a user