Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9eb38740d | ||
|
|
f37c9ec4ef | ||
|
|
d26e7b957b | ||
|
|
8b44fd5710 | ||
|
|
4e1eb7d03d | ||
|
|
e44fef7085 | ||
|
|
6f10a3b796 | ||
|
|
2c8a765036 | ||
|
|
f62b4aeec8 | ||
|
|
46dd91ce32 | ||
|
|
085e80c92c | ||
|
|
ef23f5ec32 | ||
|
|
5547c401ed | ||
|
|
fa8f1b8675 | ||
|
|
af00c2b938 | ||
|
|
1177f0ed64 | ||
|
|
35775c3d4a | ||
|
|
8ac1d11d17 | ||
|
|
b3a9192088 | ||
|
|
634f367e96 | ||
|
|
482e238a7a | ||
|
|
f31cee206b | ||
|
|
14b5a7cb68 | ||
|
|
8628ae6864 | ||
|
|
38263eaa72 | ||
|
|
ff6e1ecff4 | ||
|
|
9ccaa3f01d | ||
|
|
78f1bb0f92 | ||
|
|
5e60f9b953 | ||
|
|
3b0f4f8b66 | ||
|
|
2aaaad71a8 | ||
|
|
4719e3aa80 | ||
|
|
84d73105cf | ||
|
|
d0e03b026c | ||
|
|
d2f0a5c976 | ||
|
|
48b34c9e31 | ||
|
|
0b6eda0cdc | ||
|
|
285d2de397 | ||
|
|
8b45d8cbf6 | ||
|
|
f3b0ef4733 | ||
|
|
d20d4e2158 | ||
|
|
d321fe72cd | ||
|
|
9b09f1aee3 | ||
|
|
45931332d4 | ||
|
|
8c3250b163 | ||
|
|
d53459d047 | ||
|
|
89429039fb | ||
|
|
b47eeaf7bc | ||
|
|
a453dc9ece | ||
|
|
2c9fda3db1 | ||
|
|
5d7789eb4e | ||
|
|
bae1ac4337 | ||
|
|
643aac92f8 | ||
|
|
8194ea9e87 | ||
|
|
3afbe6c6c1 | ||
|
|
c72b5a988e | ||
|
|
2fd8ff8260 | ||
|
|
00cf8d676d |
@@ -23,14 +23,17 @@ Your state stays on your LAN: HA talks to the appliance over a direct DTLS sessi
|
||||
|---|---|
|
||||
| Air conditioner | `by_type/airconditioner.py` |
|
||||
| Air purifier | `by_type/air_purifier.py` |
|
||||
| Dehumidifier | `by_type/dehumidifier.py` |
|
||||
| Dryer | `by_type/dryer.py` |
|
||||
| Oven | `by_type/oven.py` |
|
||||
| Cooktop (read-only burner status) | `by_type/cooktop.py` |
|
||||
| Oven (including combi microwaves) | `by_type/oven.py` |
|
||||
| Gas cooktop (read-only burner status) | `by_type/cooktop.py` |
|
||||
| Range hood | `by_type/range_hood.py` |
|
||||
| Range | `by_type/range.py` |
|
||||
| Dishwasher | `by_type/dishwasher.py` |
|
||||
| Refrigerator | `by_type/refrigerator.py` |
|
||||
| Washer | `by_type/washer.py` |
|
||||
| Water purifier | `by_type/water_purifier.py` |
|
||||
| Vacuum clean/auto-empty station | `by_type/vacuum_station.py` |
|
||||
|
||||
Each registry composes shared and family-specific `Capability` objects from `registry/capabilities/`; those modules document the individual resources/entities in more depth than a README table can stay current with.
|
||||
|
||||
|
||||
@@ -20,10 +20,13 @@ back for, power, mode, temperature and wind resources alike.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
PRESET_NONE,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
@@ -45,6 +48,7 @@ from .registry.capabilities.airconditioner import (
|
||||
HREF_TEMPS_VS as TEMPS_VS_HREF,
|
||||
HREF_WIND_STRENGTH as WIND_STRENGTH_HREF,
|
||||
HREF_WIND_DIRECTION as WIND_DIRECTION_HREF,
|
||||
HREF_WIND_OSCILLATION as WIND_OSCILLATION_HREF,
|
||||
HREF_CONVENIENT as CONVENIENT_HREF,
|
||||
)
|
||||
from .registry.capabilities.common import normalize_temp_unit
|
||||
@@ -53,6 +57,8 @@ from .const import DOMAIN
|
||||
from .coordinator import LocalThingsCoordinator
|
||||
from .entity import LocalThingsEntity, _is_included
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_MODES_FIELD = 'x.com.samsung.da.modes'
|
||||
_SUPPORTED_FIELD = 'x.com.samsung.da.supportedModes'
|
||||
|
||||
@@ -62,12 +68,39 @@ _SUPPORTED_FIELD = 'x.com.samsung.da.supportedModes'
|
||||
_DEVICE_TO_HVAC: dict[str, HVACMode] = {
|
||||
'Cool': HVACMode.COOL,
|
||||
'Dry': HVACMode.DRY,
|
||||
# Fan-only is spelled 'Wind' on some boards (e.g. TP1X_DA-AC-RAC-01001) and
|
||||
# 'Fan' on others (e.g. TP1X_DA-AC-RAC-01011); both map to FAN_ONLY. The
|
||||
# reverse write can't rely on this map alone (two codes, one HA value) --
|
||||
# _device_code_for_hvac() resolves the code from the unit's own
|
||||
# supportedModes, so this is only a fallback for a unit reporting no
|
||||
# supportedModes at all. 'Fan' is listed first so the {v: k} reverse
|
||||
# comprehension below has 'Wind' win that fallback (last-key-wins),
|
||||
# preserving the original single-spelling behavior rather than silently
|
||||
# flipping it when 'Fan' was added.
|
||||
'Fan': HVACMode.FAN_ONLY,
|
||||
'Wind': HVACMode.FAN_ONLY,
|
||||
'Auto': HVACMode.HEAT_COOL,
|
||||
# The device's 'Auto' is a single-setpoint "device decides" mode -> HA
|
||||
# HVACMode.AUTO (renders "Auto"). Not HEAT_COOL: that renders "Heat/cool"
|
||||
# and implies a two-setpoint heat+cool range these single-setpoint units
|
||||
# (including cool-only models) don't have.
|
||||
'Auto': HVACMode.AUTO,
|
||||
'Heat': HVACMode.HEAT,
|
||||
}
|
||||
_HVAC_TO_DEVICE = {v: k for k, v in _DEVICE_TO_HVAC.items()}
|
||||
|
||||
# AI-driven auto-comfort mode (issue #93, A-CAWW-TP2-20-COMMON). Not a flat
|
||||
# _DEVICE_TO_HVAC entry: 'AIComfort' isn't a distinct thermodynamic operation
|
||||
# like Cool/Dry/Heat, it's an AI overlay on top of the device's own 'Auto'
|
||||
# behavior -- confirmed by this unit reporting both 'Auto' and 'AIComfort' as
|
||||
# separate, mutually-exclusive entries in /mode/vs/0's supportedModes. Modeled
|
||||
# the idiomatic HA way instead: hvac_mode reports AUTO (same as the plain
|
||||
# 'Auto' code maps to) and a dedicated 'ai_comfort' preset carries the
|
||||
# distinction a bare hvac_mode can't. Not reachable via async_set_hvac_mode --
|
||||
# entered/left only through the preset, since there's no dedicated HVACMode
|
||||
# value for it to write back to.
|
||||
_AI_COMFORT_MODE = 'AIComfort'
|
||||
PRESET_AI_COMFORT = 'ai_comfort'
|
||||
|
||||
# Fan (wind strength): device codes "0".."4" -> HA standard fan constants where
|
||||
# a clean match exists so they auto-localize; "turbo" is custom (translated).
|
||||
_DEVICE_TO_FAN: dict[str, str] = {
|
||||
@@ -84,19 +117,42 @@ _DEVICE_TO_SWING: dict[str, str] = {
|
||||
'Fix': 'off',
|
||||
'All': 'both',
|
||||
'Up_And_Low': 'vertical',
|
||||
'Left_And_Right': 'horizontal', # issue #75
|
||||
}
|
||||
_SWING_TO_DEVICE = {v: k for k, v in _DEVICE_TO_SWING.items()}
|
||||
|
||||
# Preset (convenient mode): Off/Sleep map onto HA standard presets; Quiet/Smart/
|
||||
# Speed are custom (translated).
|
||||
_DEVICE_TO_PRESET: dict[str, str] = {
|
||||
'Off': 'none',
|
||||
'Sleep': 'sleep',
|
||||
'Quiet': 'quiet',
|
||||
'Smart': 'smart',
|
||||
'Speed': 'speed',
|
||||
}
|
||||
_PRESET_TO_DEVICE = {v: k for k, v in _DEVICE_TO_PRESET.items()}
|
||||
# Swing fallback via /wind/oscillation/vs/0 (issue #126) -- boards without
|
||||
# WIND_DIRECTION_HREF at all report two independent Swing|Fix toggles
|
||||
# instead of one combined code. Same HA vocabulary as _DEVICE_TO_SWING
|
||||
# above (off/vertical/horizontal/both), just read from/written to a pair
|
||||
# of fields rather than a single one.
|
||||
def _oscillation_swing(rep: dict) -> str | None:
|
||||
vertical = rep.get('vertical')
|
||||
horizontal = rep.get('horizontal')
|
||||
if vertical is None and horizontal is None:
|
||||
return None
|
||||
v = vertical == 'Swing'
|
||||
h = horizontal == 'Swing'
|
||||
if v and h:
|
||||
return 'both'
|
||||
if v:
|
||||
return 'vertical'
|
||||
if h:
|
||||
return 'horizontal'
|
||||
return 'off'
|
||||
|
||||
# Preset (convenient mode): resolved dynamically from the device's own
|
||||
# /mode/convenient/vs/0 supportedModes -- no per-model table. The device 'Off'
|
||||
# code maps to HA's PRESET_NONE ("no preset active"); every other code is
|
||||
# exposed as its lowercased self and labelled in translations
|
||||
# (entity.climate.airconditioner.state_attributes.preset_mode.state.<code>),
|
||||
# so any board's convenient modes surface without code changes, and an
|
||||
# unlabelled code just renders as its raw value until a label is added.
|
||||
# (Samsung's WindFree still-air cooling shows up here as the 'Nano'/
|
||||
# 'NanoSleep' codes on cool-only global RAC boards -- that's just a
|
||||
# translation label, not a hard-coded mode.)
|
||||
def _preset_to_ha(code) -> str:
|
||||
return PRESET_NONE if code == 'Off' else str(code).lower()
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -164,6 +220,11 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
# (href, raw device code) pairs already logged by _warn_unmapped --
|
||||
# these properties are read on every coordinator refresh, so an
|
||||
# un-deduped warning would spam the log for any device with a
|
||||
# genuinely unrecognized code.
|
||||
self._warned_unmapped: set[tuple[str, str]] = set()
|
||||
|
||||
# -- resource helpers ---------------------------------------------------
|
||||
|
||||
@@ -171,25 +232,59 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
return self.coordinator.resource(href) or {}
|
||||
|
||||
def _is_on(self) -> bool:
|
||||
rep = self._rep(POWER_HREF)
|
||||
if 'value' in rep:
|
||||
return bool(rep.get('value'))
|
||||
vs = self._rep(POWER_VS_HREF)
|
||||
return str(vs.get('x.com.samsung.da.power', '')).lower() == 'on'
|
||||
# Prefer the vendor /power/vs/0 (present on every observed board and
|
||||
# the resource writes target -- see airconditioner._climate_write).
|
||||
# The OCF /power/0 is absent on many boards and a stale mirror on
|
||||
# some, so reading it first showed pre-write state after a power
|
||||
# toggle (issue #53: "can turn on but not off").
|
||||
power = self._rep(POWER_VS_HREF).get('x.com.samsung.da.power')
|
||||
if power is not None:
|
||||
return str(power).lower() == 'on'
|
||||
return bool(self._rep(POWER_HREF).get('value'))
|
||||
|
||||
def _supported(self, href: str) -> list[str]:
|
||||
return list(self._rep(href).get(_SUPPORTED_FIELD) or [])
|
||||
|
||||
def _warn_unmapped(self, href: str, code: str) -> None:
|
||||
"""Log once per (href, code) when a device-reported mode has no
|
||||
entry in the relevant device<->HA map, so a real device gap surfaces
|
||||
in the log instead of silently vanishing (issue #93)."""
|
||||
key = (href, code)
|
||||
if key in self._warned_unmapped:
|
||||
return
|
||||
self._warned_unmapped.add(key)
|
||||
_LOGGER.warning(
|
||||
"%s: device mode %r on %s has no HA mapping and was dropped; "
|
||||
"please file an issue with your diagnostics dump",
|
||||
self.entity_id, code, href,
|
||||
)
|
||||
|
||||
def _read_mode(self, href: str, mapping: dict):
|
||||
"""Current mode of a wind/convenient resource, mapped to its HA value."""
|
||||
return mapping.get(_first(self._rep(href).get(_MODES_FIELD)))
|
||||
raw = _first(self._rep(href).get(_MODES_FIELD))
|
||||
if raw is not None and raw not in mapping:
|
||||
self._warn_unmapped(href, raw)
|
||||
return mapping.get(raw)
|
||||
|
||||
def _read_modes(self, href: str, mapping: dict) -> list[str]:
|
||||
"""Supported modes of a resource, mapped to HA values (unknowns dropped)."""
|
||||
return [mapping[c] for c in self._supported(href) if c in mapping]
|
||||
supported = self._supported(href)
|
||||
for c in supported:
|
||||
if c not in mapping:
|
||||
self._warn_unmapped(href, c)
|
||||
return [mapping[c] for c in supported if c in mapping]
|
||||
|
||||
# -- temperature --------------------------------------------------------
|
||||
|
||||
def _ocf_temp_authoritative(self) -> bool:
|
||||
"""True when the OCF /temperature/{current,desired}/0 pair is the
|
||||
authoritative temperature channel -- signalled by
|
||||
/temperature/current/0 being present. Those boards honour reads/
|
||||
writes on /temperature/desired/0 and ignore the vendor
|
||||
/temperatures/vs/0; boards without the pair (only a desired stub, or
|
||||
nothing) are the reverse. Confirmed on live units of both kinds."""
|
||||
return bool(self._rep(TEMP_CURRENT_HREF))
|
||||
|
||||
def _temps_vs(self) -> dict:
|
||||
"""Vendor `/temperatures/vs/0` items[0] (empty {} when absent)."""
|
||||
return _temps_vs_item(self._rep(TEMPS_VS_HREF))
|
||||
@@ -212,10 +307,14 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
v = _num(self._rep(TEMP_DESIRED_HREF).get('temperature'))
|
||||
if v is None:
|
||||
v = _num(self._temps_vs().get('x.com.samsung.da.desired'))
|
||||
return v
|
||||
# Read from the same channel writes go to (see async_set_temperature):
|
||||
# OCF /temperature/desired/0 on boards with the full OCF pair, vendor
|
||||
# /temperatures/vs/0 otherwise -- with the other as fallback.
|
||||
ocf = _num(self._rep(TEMP_DESIRED_HREF).get('temperature'))
|
||||
vs = _num(self._temps_vs().get('x.com.samsung.da.desired'))
|
||||
if self._ocf_temp_authoritative():
|
||||
return ocf if ocf is not None else vs
|
||||
return vs if vs is not None else ocf
|
||||
|
||||
def _range(self) -> list | None:
|
||||
r = self._rep(TEMP_DESIRED_HREF).get('range')
|
||||
@@ -250,14 +349,23 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
if not self._is_on():
|
||||
return HVACMode.OFF
|
||||
device = _first(self._rep(MODE_HREF).get(_MODES_FIELD))
|
||||
if device == _AI_COMFORT_MODE:
|
||||
return HVACMode.AUTO
|
||||
if device is not None and device not in _DEVICE_TO_HVAC:
|
||||
self._warn_unmapped(MODE_HREF, device)
|
||||
return _DEVICE_TO_HVAC.get(device, HVACMode.AUTO)
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
modes = [HVACMode.OFF]
|
||||
for m in self._supported(MODE_HREF):
|
||||
if m == _AI_COMFORT_MODE:
|
||||
continue
|
||||
mapped = _DEVICE_TO_HVAC.get(m)
|
||||
if mapped is not None and mapped not in modes:
|
||||
if mapped is None:
|
||||
self._warn_unmapped(MODE_HREF, m)
|
||||
continue
|
||||
if mapped not in modes:
|
||||
modes.append(mapped)
|
||||
return modes
|
||||
|
||||
@@ -271,34 +379,66 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
def fan_modes(self) -> list[str]:
|
||||
return self._read_modes(WIND_STRENGTH_HREF, _DEVICE_TO_FAN)
|
||||
|
||||
def _swing_via_direction(self) -> bool:
|
||||
"""True when WIND_DIRECTION_HREF is the swing channel to use --
|
||||
signalled by its presence. Boards without it (issue #126) report
|
||||
the 2-axis oscillation resource instead; see _oscillation_swing."""
|
||||
return bool(self._rep(WIND_DIRECTION_HREF))
|
||||
|
||||
@property
|
||||
def swing_mode(self):
|
||||
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._swing_via_direction():
|
||||
return self._read_modes(WIND_DIRECTION_HREF, _DEVICE_TO_SWING)
|
||||
if self._rep(WIND_OSCILLATION_HREF):
|
||||
return list(_SWING_TO_DEVICE.keys())
|
||||
return []
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
return self._read_mode(CONVENIENT_HREF, _DEVICE_TO_PRESET)
|
||||
if _first(self._rep(MODE_HREF).get(_MODES_FIELD)) == _AI_COMFORT_MODE:
|
||||
return PRESET_AI_COMFORT
|
||||
code = _first(self._rep(CONVENIENT_HREF).get(_MODES_FIELD))
|
||||
return _preset_to_ha(code) if code is not None else None
|
||||
|
||||
@property
|
||||
def preset_modes(self) -> list[str]:
|
||||
return self._read_modes(CONVENIENT_HREF, _DEVICE_TO_PRESET)
|
||||
modes = [_preset_to_ha(c) for c in self._supported(CONVENIENT_HREF)]
|
||||
if _AI_COMFORT_MODE in self._supported(MODE_HREF):
|
||||
modes.append(PRESET_AI_COMFORT)
|
||||
return modes
|
||||
|
||||
# -- writes -------------------------------------------------------------
|
||||
|
||||
def _device_code_for_hvac(self, hvac_mode: HVACMode):
|
||||
"""Device mode code for an HA hvac_mode, chosen from this unit's own
|
||||
supportedModes -- fan-only is 'Wind' on some boards and 'Fan' on
|
||||
others, so the reverse map alone can't pick the code this unit
|
||||
accepts."""
|
||||
for code in self._supported(MODE_HREF):
|
||||
if _DEVICE_TO_HVAC.get(code) == hvac_mode:
|
||||
return code
|
||||
return _HVAC_TO_DEVICE.get(hvac_mode)
|
||||
|
||||
async def async_set_temperature(self, **kwargs) -> None:
|
||||
temp = kwargs.get('temperature')
|
||||
if temp is not None:
|
||||
await self.coordinator.async_send_command(self._bound, ('temperature', temp))
|
||||
if temp is None:
|
||||
return
|
||||
# OCF-pair boards write /temperature/desired/0; vendor boards write
|
||||
# /temperatures/vs/0 (see airconditioner._climate_write).
|
||||
kind = 'temperature_ocf' if self._ocf_temp_authoritative() else 'temperature'
|
||||
await self.coordinator.async_send_command(self._bound, (kind, temp))
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self.coordinator.async_send_command(self._bound, ('power', False))
|
||||
return
|
||||
device = _HVAC_TO_DEVICE.get(hvac_mode)
|
||||
device = self._device_code_for_hvac(hvac_mode)
|
||||
if device is None:
|
||||
return
|
||||
if not self._is_on():
|
||||
@@ -321,7 +461,23 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
await self._set_mapped('fan', _FAN_TO_DEVICE, fan_mode)
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
if self._swing_via_direction():
|
||||
await self._set_mapped('swing', _SWING_TO_DEVICE, swing_mode)
|
||||
return
|
||||
if self._rep(WIND_OSCILLATION_HREF):
|
||||
await self.coordinator.async_send_command(
|
||||
self._bound, ('oscillation', swing_mode))
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
await self._set_mapped('preset', _PRESET_TO_DEVICE, preset_mode)
|
||||
if preset_mode == PRESET_AI_COMFORT:
|
||||
# Writes the primary mode resource, not the convenient one --
|
||||
# 'AIComfort' lives in /mode/vs/0 alongside Cool/Dry/Auto, not in
|
||||
# /mode/convenient/vs/0 with Quiet/Smart/Speed/Sleep.
|
||||
await self.coordinator.async_send_command(self._bound, ('mode', _AI_COMFORT_MODE))
|
||||
return
|
||||
# Reverse-resolve against the unit's own supportedModes (codes aren't
|
||||
# a fixed transform of the HA value -- e.g. 'NanoSleep' -> 'nanosleep').
|
||||
for code in self._supported(CONVENIENT_HREF):
|
||||
if _preset_to_ha(code) == preset_mode:
|
||||
await self.coordinator.async_send_command(self._bound, ('preset', code))
|
||||
return
|
||||
|
||||
@@ -197,6 +197,18 @@ def _find_live_ports(host: str, ports: list[int], timeout: float) -> list[int]:
|
||||
return _order_candidates(live)
|
||||
|
||||
|
||||
def _is_placeholder_serial(serial: str) -> bool:
|
||||
"""True for a non-empty serialNum that isn't actually a real identity.
|
||||
|
||||
The ARTIK051_DONGLE_REF firmware family reports the literal string
|
||||
'Nothing(SVC)' for every unit -- non-empty, so the plain `if not
|
||||
serial` check here (and the equivalent one in coordinator.py's
|
||||
`_run_discovery`) doesn't catch it, and two such units get the same
|
||||
config-entry unique_id / entity unique_ids and collide (issue #83).
|
||||
"""
|
||||
return serial.strip().lower().startswith('nothing')
|
||||
|
||||
|
||||
def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict:
|
||||
"""Fetch UUID, mint leaf cert, probe each port. Returns config entry data dict."""
|
||||
import cbor2
|
||||
@@ -261,7 +273,7 @@ def _probe_and_validate(host: str, ca_cert_pem: str, ca_key_pem: str) -> dict:
|
||||
.get('/information/vs/0', {})
|
||||
.get('x.com.samsung.da.serialNum', '')
|
||||
)
|
||||
if not serial:
|
||||
if not serial or _is_placeholder_serial(serial):
|
||||
serial = f"{host}:{port}"
|
||||
one_ui_version = (
|
||||
resources
|
||||
|
||||
@@ -22,7 +22,12 @@ from smartthings_local.ocf.state_cache import StateCache
|
||||
|
||||
from .registry.batch import parse_device0_batch
|
||||
from .registry.by_type import for_device, for_device_by_model, for_device_by_resources
|
||||
from .registry.capabilities.common import merge_options_field, remote_control_enabled
|
||||
from .registry.capabilities.common import (
|
||||
merge_items_field,
|
||||
merge_options_field,
|
||||
remote_control_enabled,
|
||||
remote_control_required_for_write,
|
||||
)
|
||||
from .registry.discovery import discover, BoundEntity
|
||||
from .registry import CAPABILITIES
|
||||
from .registry.adapter import flatten
|
||||
@@ -49,6 +54,22 @@ class _NoOpDescriptor:
|
||||
_RECOVERY_RETRY_S = 600.0 # re-attempt observe mode this often while polling
|
||||
|
||||
|
||||
def _is_placeholder_serial(serial: str) -> bool:
|
||||
"""True for a non-empty serialNum that isn't actually a real identity.
|
||||
|
||||
The ARTIK051_DONGLE_REF firmware family reports the literal string
|
||||
'Nothing(SVC)' for every unit -- non-empty, so the plain `if not
|
||||
serial` check below doesn't catch it, and `device_serial` feeds both
|
||||
the HA device-registry identifier and every entity's unique_id
|
||||
(entity.py), so two such units on the same install silently collide
|
||||
and the second one's entities get dropped (issue #83). Mirrors the
|
||||
identical helper in config_flow.py's `_probe_and_validate` -- kept
|
||||
separate rather than imported to avoid pulling the config-flow module
|
||||
into the runtime coordinator's import graph for a two-line check.
|
||||
"""
|
||||
return serial.strip().lower().startswith('nothing')
|
||||
|
||||
|
||||
class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Manages one Samsung appliance: session, discovery, polling."""
|
||||
|
||||
@@ -64,6 +85,24 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
_OBSERVE_GRACE_PERIOD_S: float = GRACE_PERIOD_S
|
||||
_RECONNECT_PAUSE_S: float = 5.0
|
||||
|
||||
# A single reconnect is normal appliance-side behavior (see the
|
||||
# README's "Known device behavior" section) -- Samsung's firmware
|
||||
# drops the DTLS session briefly every now and then, and the
|
||||
# coordinator recovering from that on its own isn't something a user
|
||||
# needs to see at WARNING. Only escalate once reconnects pile up
|
||||
# within a trailing window (issue #119).
|
||||
#
|
||||
# The window can't be a literal 60s: consecutive reconnect attempts are
|
||||
# never closer together than one summary poll interval (SUMMARY_INTERVAL_S,
|
||||
# 30s) plus _RECONNECT_PAUSE_S, so at most ~2 can ever land inside a 60s
|
||||
# window regardless of how unhealthy the connection is -- a threshold of
|
||||
# 5 there could never fire, silently downgrading every reconnect
|
||||
# (including a persistently broken one) to INFO forever. 300s/3 instead:
|
||||
# reachable under normal polling, and 3 reconnects inside 5 minutes is
|
||||
# still a reasonable proxy for the README's "actually broken" case.
|
||||
_RECONNECT_WARN_WINDOW_S: float = 300.0
|
||||
_RECONNECT_WARN_THRESHOLD: int = 3
|
||||
|
||||
# A block-level ACK timeout on the summary GET doesn't prove the
|
||||
# session is dead (see _poll_once) — require this many in a row
|
||||
# before treating it as one. A single slow transfer on an otherwise
|
||||
@@ -115,6 +154,7 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self.one_ui_version: str = ''
|
||||
self._consecutive_poll_timeouts = 0
|
||||
self._unbound_hrefs: list[str] = []
|
||||
self._reconnect_times: list[float] = []
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Session management (all blocking — must run in executor)
|
||||
@@ -312,7 +352,7 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self._unbound_hrefs = unbound
|
||||
|
||||
serial = info.get('x.com.samsung.da.serialNum', '')
|
||||
if not serial:
|
||||
if not serial or _is_placeholder_serial(serial):
|
||||
serial = self._entry.data[CONF_HOST]
|
||||
self.device_serial = serial
|
||||
|
||||
@@ -423,6 +463,17 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self._consecutive_poll_timeouts += 1
|
||||
return self._consecutive_poll_timeouts < self._POLL_TIMEOUT_LIMIT
|
||||
|
||||
def _reconnect_is_frequent(self) -> bool:
|
||||
"""True once this cycle's reconnect is the Nth within the trailing
|
||||
warn window -- see _RECONNECT_WARN_WINDOW_S/_RECONNECT_WARN_THRESHOLD."""
|
||||
now = time.monotonic()
|
||||
self._reconnect_times = [
|
||||
t for t in self._reconnect_times
|
||||
if now - t < self._RECONNECT_WARN_WINDOW_S
|
||||
]
|
||||
self._reconnect_times.append(now)
|
||||
return len(self._reconnect_times) >= self._RECONNECT_WARN_THRESHOLD
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DataUpdateCoordinator hook
|
||||
# ------------------------------------------------------------------
|
||||
@@ -449,7 +500,13 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self._consecutive_poll_timeouts = 0
|
||||
# One reconnect attempt — pause briefly so the device can
|
||||
# clean up its DTLS session state before we knock again.
|
||||
# A lone reconnect is routine (see the README's "Known
|
||||
# device behavior" section); only warn once they're piling
|
||||
# up within the trailing window.
|
||||
if self._reconnect_is_frequent():
|
||||
self._log.warning("poll failed, reconnecting: %s", e)
|
||||
else:
|
||||
self._log.info("poll failed, reconnecting: %s", e)
|
||||
await self.hass.async_add_executor_job(self._close_session)
|
||||
await asyncio.sleep(self._RECONNECT_PAUSE_S)
|
||||
try:
|
||||
@@ -531,7 +588,10 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
opted this device out of it via CONF_BYPASS_REMOTE_CONTROL (issue
|
||||
#54: some devices accept certain writes, e.g. a washer's default
|
||||
dosing levels, even while reporting remote control off, so the
|
||||
block's assumption doesn't hold for every model)."""
|
||||
block's assumption doesn't hold for every model), or the laundry
|
||||
firmware flag isModelSettingWithoutSC declares settings writable
|
||||
without Smart Control (cycle start/pause/stop on /operational/state
|
||||
still require it)."""
|
||||
desc = bound_entity.desc
|
||||
write_fn = getattr(desc, 'write_fn', None)
|
||||
if write_fn is None:
|
||||
@@ -540,7 +600,11 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
rep = self._cache.get(href or '') or {}
|
||||
resources = self._cache.snapshot()
|
||||
bypass_remote_control = self._entry.options.get(CONF_BYPASS_REMOTE_CONTROL, False)
|
||||
if not bypass_remote_control and not remote_control_enabled(resources):
|
||||
if (
|
||||
not bypass_remote_control
|
||||
and remote_control_required_for_write(resources, href or '')
|
||||
and not remote_control_enabled(resources)
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="remote_control_disabled",
|
||||
@@ -553,6 +617,9 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=error,
|
||||
)
|
||||
try:
|
||||
result = write_fn(payload, rep, href, resources)
|
||||
except TypeError:
|
||||
result = write_fn(payload, rep, href)
|
||||
if result is None:
|
||||
self._log.warning("write_fn rejected payload %r for %s", payload, href)
|
||||
@@ -611,23 +678,35 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
# introduced its own races around overlapping writes to the same
|
||||
# href. Simpler and safer to just hold the guard for the full,
|
||||
# generously-sized window and let it expire on its own.
|
||||
# write_fn bodies that touch x.com.samsung.da.options carry only the
|
||||
# changed token(s) now (issue #54: confirmed sufficient on the wire --
|
||||
# the device merges by prefix itself), not the whole packed array.
|
||||
# observe.apply()'s field-level {**cached, **rep} merge doesn't know
|
||||
# that -- handed the bare token list, it would replace the cached
|
||||
# field outright and wipe every sibling option for the rest of the
|
||||
# settle window. Pre-merge it here the same way the device does, so
|
||||
# the optimistic cache entry stays complete; the minimal `body` below
|
||||
# is still exactly what goes out over the wire.
|
||||
# write_fn bodies that touch x.com.samsung.da.options or
|
||||
# x.com.samsung.da.items carry only the changed token(s)/item now
|
||||
# (issue #54 for options; the AC vendor temperature write for items --
|
||||
# confirmed sufficient on the wire, the device merges the rest itself),
|
||||
# not the whole packed array. observe.apply()'s field-level
|
||||
# {**cached, **rep} merge doesn't know that -- handed the bare
|
||||
# partial value, it would replace the cached field outright and wipe
|
||||
# every sibling option/item for the rest of the settle window.
|
||||
# Pre-merge it here the same way the device does, so the optimistic
|
||||
# cache entry stays complete; the minimal `body` below is still
|
||||
# exactly what goes out over the wire.
|
||||
optimistic_body = body
|
||||
new_options = body.get('x.com.samsung.da.options')
|
||||
if isinstance(new_options, list):
|
||||
cached_options = (self._cache.get(write_href) or {}).get('x.com.samsung.da.options')
|
||||
optimistic_body = {
|
||||
**body,
|
||||
**optimistic_body,
|
||||
'x.com.samsung.da.options': merge_options_field(cached_options, new_options),
|
||||
}
|
||||
# Same fact, items[] shape (e.g. airconditioner._climate_write's vendor
|
||||
# temperature write, which now carries only {id, desired} -- see that
|
||||
# module for the write-side half of this).
|
||||
new_items = body.get('x.com.samsung.da.items')
|
||||
if isinstance(new_items, list):
|
||||
cached_items = (self._cache.get(write_href) or {}).get('x.com.samsung.da.items')
|
||||
optimistic_body = {
|
||||
**optimistic_body,
|
||||
'x.com.samsung.da.items': merge_items_field(cached_items, new_items),
|
||||
}
|
||||
self._observe.apply(write_href, optimistic_body, source='optimistic')
|
||||
self._observe.mark_write_pending(
|
||||
write_href, settle_s=self._POST_TIMEOUT_S + self._POLL_TIMEOUT_S
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
"""Fan platform for Samsung range hoods."""
|
||||
"""Fan platform for Samsung range hoods and air purifiers.
|
||||
|
||||
Two FanDesc-bound hrefs exist, one per family, dispatched by href in
|
||||
async_setup_entry below since they need different HA fan semantics: the
|
||||
range hood's fan speed is an ordered set of numeric levels (SET_SPEED), while
|
||||
the newer air-purifier board family's modes (Smart/Max/Mid/WindFree/Sleep,
|
||||
issue #130) are named behaviors with no linear order (PRESET_MODE)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -14,14 +22,19 @@ from homeassistant.util.percentage import (
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LocalThingsCoordinator
|
||||
from .entity import LocalThingsEntity, _is_included
|
||||
from .registry.capabilities.air_purifier import HREF_MODE as AIR_PURIFIER_FAN_HREF
|
||||
from .registry.entities import FanDesc
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
POWER_HREF = '/power/0'
|
||||
POWER_VS_HREF = '/power/vs/0'
|
||||
_FAN_SPEED_FIELD = 'x.com.samsung.da.hood.fanSpeed'
|
||||
_SUPPORTED_FAN_SPEED_FIELD = 'x.com.samsung.da.hood.supportedFanSpeed'
|
||||
|
||||
_MODES_FIELD = 'x.com.samsung.da.modes'
|
||||
_SUPPORTED_MODES_FIELD = 'x.com.samsung.da.supportedModes'
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -29,11 +42,15 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities(
|
||||
LocalThingsRangeHoodFan(coordinator, bound)
|
||||
for bound in coordinator.bound
|
||||
if isinstance(bound.desc, FanDesc) and _is_included(bound, coordinator)
|
||||
)
|
||||
entities = []
|
||||
for bound in coordinator.bound:
|
||||
if not (isinstance(bound.desc, FanDesc) and _is_included(bound, coordinator)):
|
||||
continue
|
||||
if bound.href == AIR_PURIFIER_FAN_HREF:
|
||||
entities.append(LocalThingsAirPurifierFan(coordinator, bound))
|
||||
else:
|
||||
entities.append(LocalThingsRangeHoodFan(coordinator, bound))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class LocalThingsRangeHoodFan(LocalThingsEntity, FanEntity):
|
||||
@@ -120,3 +137,80 @@ class LocalThingsRangeHoodFan(LocalThingsEntity, FanEntity):
|
||||
)
|
||||
code = percentage_to_ordered_list_item(codes, percentage)
|
||||
await self.coordinator.async_send_command(self._bound, ('speed', code))
|
||||
|
||||
|
||||
class LocalThingsAirPurifierFan(LocalThingsEntity, FanEntity):
|
||||
"""Air-purifier fan: named preset modes, not an ordered percentage --
|
||||
see air_purifier.FAN's comment for why (Smart/WindFree/Sleep aren't
|
||||
"faster/slower" than Max/Mid)."""
|
||||
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
_attr_supported_features = (
|
||||
FanEntityFeature.PRESET_MODE
|
||||
| FanEntityFeature.TURN_ON
|
||||
| FanEntityFeature.TURN_OFF
|
||||
)
|
||||
|
||||
def __init__(self, coordinator: LocalThingsCoordinator, bound) -> None:
|
||||
super().__init__(coordinator, bound)
|
||||
self._attr_name = None
|
||||
|
||||
def _rep(self, href: str) -> dict:
|
||||
return self.coordinator.resource(href) or {}
|
||||
|
||||
def _mode_rep(self) -> dict:
|
||||
return self._rep(self._bound.href)
|
||||
|
||||
def _power_payload(self, enabled: bool) -> tuple[str, bool, str]:
|
||||
"""Target whichever power resource this unit actually exposes --
|
||||
same pattern as LocalThingsRangeHoodFan._power_payload above.
|
||||
Writing a hardcoded href here would silently no-op on a board that
|
||||
only reports the other one, even though is_on already falls back
|
||||
correctly."""
|
||||
resources = self.coordinator.last_resources
|
||||
target = POWER_VS_HREF if POWER_VS_HREF in resources else POWER_HREF
|
||||
return 'power', enabled, target
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
power = self._rep(POWER_VS_HREF).get('x.com.samsung.da.power')
|
||||
if power is not None:
|
||||
return str(power).lower() == 'on'
|
||||
return bool(self._rep(POWER_HREF).get('value'))
|
||||
|
||||
@property
|
||||
def preset_modes(self) -> list[str]:
|
||||
return [
|
||||
str(code).lower()
|
||||
for code in self._mode_rep().get(_SUPPORTED_MODES_FIELD, ())
|
||||
]
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
modes = self._mode_rep().get(_MODES_FIELD)
|
||||
code = modes[0] if isinstance(modes, (list, tuple)) and modes else modes
|
||||
return str(code).lower() if code is not None else None
|
||||
|
||||
async def async_turn_on(
|
||||
self, percentage: int | None = None, preset_mode: str | None = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
await self.coordinator.async_send_command(self._bound, self._power_payload(True))
|
||||
if preset_mode is not None:
|
||||
await self.async_set_preset_mode(preset_mode)
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
await self.coordinator.async_send_command(self._bound, self._power_payload(False))
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
# Reverse-resolve against the unit's own supportedModes -- the
|
||||
# write needs the raw device code (e.g. 'WindFree'), not the
|
||||
# lowercased HA value.
|
||||
for code in self._mode_rep().get(_SUPPORTED_MODES_FIELD, ()):
|
||||
if str(code).lower() == preset_mode:
|
||||
await self.coordinator.async_send_command(self._bound, ('mode', code))
|
||||
return
|
||||
_LOGGER.warning(
|
||||
"%s: %r is not a valid preset mode (supported: %s)",
|
||||
self.entity_id, preset_mode, self.preset_modes,
|
||||
)
|
||||
|
||||
@@ -3,8 +3,9 @@ from typing import Optional
|
||||
|
||||
from ._base import DeviceRegistry
|
||||
from . import (
|
||||
air_purifier, airconditioner, cooktop, dishwasher, dryer, oven,
|
||||
range as _range, range_hood, refrigerator, washer,
|
||||
air_purifier, airconditioner, cooktop, dehumidifier, dishwasher, dryer,
|
||||
induction_cooktop, oven, range as _range, range_hood, refrigerator,
|
||||
vacuum_station, washer, water_purifier,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -19,14 +20,18 @@ _REGISTRY_BY_KEY: dict[str, DeviceRegistry] = {
|
||||
'airconditioner': airconditioner.REGISTRY,
|
||||
'air_conditioner': airconditioner.REGISTRY,
|
||||
'cooktop': cooktop.REGISTRY,
|
||||
'dehumidifier': dehumidifier.REGISTRY,
|
||||
'dishwasher': dishwasher.REGISTRY,
|
||||
'dryer': dryer.REGISTRY,
|
||||
'induction_cooktop': induction_cooktop.REGISTRY,
|
||||
'oven': oven.REGISTRY,
|
||||
'hood': range_hood.REGISTRY,
|
||||
'range': _range.REGISTRY,
|
||||
'range_hood': range_hood.REGISTRY,
|
||||
'refrigerator': refrigerator.REGISTRY,
|
||||
'vacuum_station': vacuum_station.REGISTRY,
|
||||
'washer': washer.REGISTRY,
|
||||
'water_purifier': water_purifier.REGISTRY,
|
||||
}
|
||||
|
||||
|
||||
@@ -82,11 +87,55 @@ _CONSUMER_PREFIX_TO_KEY: dict[str, str] = {
|
||||
'WD': 'washer',
|
||||
'WF': 'washer',
|
||||
'WV': 'washer', # FlexWash twin units (e.g. WV55M9600AW) -- issue #19
|
||||
'WA': 'washer', # Top-load washers (e.g. WA8000T) -- issue #106
|
||||
'DV': 'dryer',
|
||||
'DW': 'dishwasher',
|
||||
}
|
||||
|
||||
|
||||
def _model_num_segments(model_num: str) -> list[str]:
|
||||
"""Underscore-delimited segments of modelNum's pipe-prefix.
|
||||
|
||||
Most boards wrap a token in underscores on both sides ('..._REF_...'),
|
||||
which a plain substring check catches fine. But the ARTIK051_DONGLE_REF
|
||||
family (issues #77, #83) reports modelNum as
|
||||
'<board>_DONGLE_REF|<rest...>' -- REF is the *last* segment before the
|
||||
pipe, with no trailing underscore, so '_REF_' never matches and the
|
||||
device silently fell back to 'unknown'. Splitting on '_' and checking
|
||||
segment membership catches both shapes without caring which side (if
|
||||
either) has a delimiter.
|
||||
"""
|
||||
prefix = (model_num or '').split('|', 1)[0]
|
||||
return prefix.split('_')
|
||||
|
||||
|
||||
def _consumer_model_key(description: str) -> Optional[str]:
|
||||
"""Registry key from the consumer-model token in `description`, or None.
|
||||
|
||||
Usually that token is the last '_'-delimited segment before any
|
||||
'/board-info' suffix (e.g. '..._WW90DG6U25LEU4' -> 'WW90DG6U25LEU4').
|
||||
But issue #79's dryer pairs two model numbers in one description --
|
||||
'..._DVE50A8800_8600/DC92-...' -- so the true consumer token
|
||||
('DVE50A8800') sits one segment *before* the actual last segment
|
||||
('8600', a bare second model number with no recognizable prefix). Scan
|
||||
segments from the end and take the first one that resolves, rather
|
||||
than assuming the last segment is always it.
|
||||
|
||||
Only a 2-letter *prefix* match -- e.g. 'WAC' (the Window Air Conditioner
|
||||
board-family token, issue #87) also starts with 'WA' (the top-load-washer
|
||||
prefix, issue #106) at this granularity. for_device_by_model() calls the
|
||||
board-family modelNum checks first and this function only as a fallback,
|
||||
so that ambiguity resolves correctly without this function needing to
|
||||
know about unrelated device families.
|
||||
"""
|
||||
segments = (description or '').split('/', 1)[0].split('_')
|
||||
for segment in reversed(segments):
|
||||
key = _CONSUMER_PREFIX_TO_KEY.get(segment[:2].upper())
|
||||
if key is not None:
|
||||
return key
|
||||
return None
|
||||
|
||||
|
||||
def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegistry]:
|
||||
"""Fallback device-type detection for hardware that never reports
|
||||
oneUiVersion (confirmed for washers -- their /otninformation/vs/0 has
|
||||
@@ -100,18 +149,30 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
||||
DeviceRegistry if the consumer-model code or modelNum resolves to a
|
||||
known type, None otherwise.
|
||||
"""
|
||||
token = (description or '').split('/', 1)[0].rsplit('_', 1)[-1]
|
||||
key = _CONSUMER_PREFIX_TO_KEY.get(token[:2].upper())
|
||||
if key is None and '_REF_' in (model_num or ''):
|
||||
# Board-family modelNum tokens are checked first, and the fuzzier
|
||||
# 2-letter consumer-model prefix from `description` only as a fallback
|
||||
# (see the bottom of this function) -- some board tokens are themselves
|
||||
# only 2-3 letters long ('WAC', issue #87) and would otherwise collide
|
||||
# with an unrelated consumer prefix at that granularity ('WA', issue #106).
|
||||
key = None
|
||||
if key is None and 'REF' in _model_num_segments(model_num):
|
||||
key = 'refrigerator'
|
||||
# Room air conditioners (e.g. ARTIK051_PRAC_20K) report no oneUiVersion and
|
||||
# a modelNum carrying the '_PRAC_' (Package Room Air Conditioner) token.
|
||||
if key is None and '_PRAC_' in (model_num or ''):
|
||||
key = 'airconditioner'
|
||||
# Older/simpler RAC boards (e.g. TP2X_RAC_20K, issue #37) use the plain
|
||||
# '_RAC_' token instead -- distinct from '_PRAC_' above (no overlap: the
|
||||
# 'P' sits between the underscore and 'RAC' in that token).
|
||||
if key is None and '_RAC_' in (model_num or ''):
|
||||
# Other RAC boards carry a bare 'RAC' (Room Air Conditioner) token in the
|
||||
# modelNum, in one of two spellings: the underscore form '_RAC_' (e.g.
|
||||
# TP2X_RAC_20K, issue #37) or the hyphenated form '-RAC-' (e.g.
|
||||
# TP1X_DA-AC-RAC-01001, a cool-only global variant, issue #91). Both are
|
||||
# distinct from '_PRAC_' above ('P' sits before 'RAC' with no delimiter)
|
||||
# and from range ('-RANGE-') / oven ('-OVEN-') tokens. Most TP1X boards
|
||||
# self-report oneUiVersion and resolve via for_device() upstream of this
|
||||
# fallback; the hyphenated match is what rescues variants whose
|
||||
# /otninformation/vs/0 ships no swVersionInfo block at all, so
|
||||
# oneUiVersion is empty.
|
||||
if key is None and ('_RAC_' in (model_num or '')
|
||||
or '-RAC-' in (model_num or '').upper()):
|
||||
key = 'airconditioner'
|
||||
# System air conditioners (multi-indoor-unit commercial installs, e.g.
|
||||
# A-CAWW-TP2-20-COMMON, issue #52) report no oneUiVersion either and
|
||||
@@ -122,6 +183,32 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
||||
# resource (see airconditioner.py's _AC_IGNORED).
|
||||
if key is None and '-CAWW-' in (model_num or '').upper():
|
||||
key = 'airconditioner'
|
||||
# Dehumidifiers (e.g. TP1X_DA_AC_DHM_01001_0000, issue #88) share the
|
||||
# DA_AC_ board family with the room-AC models above but carry the
|
||||
# '_DHM_' (DeHuMidifier) token instead of '_RAC_'/'_PRAC_'. Distinct
|
||||
# registry: target humidity, not temperature, is the primary control,
|
||||
# and there's no climate composite.
|
||||
if key is None and '_DHM_' in (model_num or ''):
|
||||
key = 'dehumidifier'
|
||||
# Window air conditioners (e.g. TP1X_DA_AC_WAC_01001_0000, issue #87)
|
||||
# report no oneUiVersion and carry the '_WAC_' (Window Air Conditioner)
|
||||
# token instead of '_RAC_'/'_PRAC_'. Same TP1X-class resource surface
|
||||
# as the room-AC models above (mode/convenient/wind/temperature/power/
|
||||
# filter/humidity all confirmed against the issue #87 dump binding
|
||||
# cleanly against the existing airconditioner registry once routed
|
||||
# here) -- no WAC-specific resources needed.
|
||||
if key is None and '_WAC_' in (model_num or ''):
|
||||
key = 'airconditioner'
|
||||
# ARA-WW-class wall-mount RACs (e.g. ARA-WW-TP1-22-COMMON, issues #115/
|
||||
# #116/#117/#120) report no oneUiVersion and no '_RAC_'/'-RAC-' token at
|
||||
# all -- the board family is spelled 'ARA-WW-' instead. Same resource
|
||||
# surface as the other TP1X-class room ACs above (mode/convenient/wind/
|
||||
# temperature/power/filter/humidity all confirmed against these dumps
|
||||
# binding cleanly against the existing airconditioner registry once
|
||||
# routed here) -- no ARA-WW-specific resources needed, so this reuses
|
||||
# the same registry rather than adding a new device type.
|
||||
if key is None and 'ARA-WW-' in (model_num or '').upper():
|
||||
key = 'airconditioner'
|
||||
# Air purifiers (e.g. ARTIK051_TVTL_18K, issue #56) report no
|
||||
# oneUiVersion either, and carry the '_TVTL_' board-family token.
|
||||
if key is None and '_TVTL_' in (model_num or ''):
|
||||
@@ -129,6 +216,10 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
||||
model_identity = f'{model_num} {description}'.upper()
|
||||
if key is None and ('_COOKTOP' in model_identity or '_GB_CT_' in model_identity):
|
||||
key = 'cooktop'
|
||||
# Water purifiers (e.g. TP2X_WATERPURIFIER_20K, issue #90) report no
|
||||
# oneUiVersion and carry the 'WATERPURIFIER' board-family token.
|
||||
if key is None and 'WATERPURIFIER' in model_identity:
|
||||
key = 'water_purifier'
|
||||
if key is None and model_identity.startswith('AHD-'):
|
||||
key = 'range_hood'
|
||||
# Range/cooktop-oven combos (e.g. TP1X_DA-KS-RANGE-0102X, issue #44) --
|
||||
@@ -141,6 +232,37 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
||||
# oneUiVersion and doesn't match the washer/dryer/dishwasher prefix map.
|
||||
if key is None and '-OVEN-' in (model_num or '').upper():
|
||||
key = 'oven'
|
||||
# Combi microwaves (e.g. TP1X_DA-KS-MICROWAVE-01041, issue #121) -- same
|
||||
# board family as the wall oven above (an '/oven/vs/0' cavity resource
|
||||
# with Convection/AirFryer/Grill/MicroWave modes on /mode/vs/0), just a
|
||||
# different cavity. Reports no oneUiVersion and doesn't match the
|
||||
# washer/dryer/dishwasher prefix map either. Binds cleanly against the
|
||||
# existing oven registry once routed here (confirmed against the issue
|
||||
# #121 dump) -- no microwave-specific device type needed.
|
||||
if key is None and '-MICROWAVE-' in (model_num or '').upper():
|
||||
key = 'oven'
|
||||
# Standalone induction cooktops (e.g. TP1X_DA-KS-COOKTOP-01011, issue
|
||||
# #86) -- same board family and '/cooktop/status/vs/0' resource shape
|
||||
# as the range combo above, but no oven attached at all. Distinct from
|
||||
# the '_COOKTOP'/'_GB_CT_' underscore-delimited check above: that one
|
||||
# matches a different, older gas-cooktop family (cooktop.py's NA9300K
|
||||
# class, burner state embedded in /mode/vs/0's options array) whose
|
||||
# modelNum token is underscore-delimited, not hyphenated like this
|
||||
# board family's.
|
||||
if key is None and '-COOKTOP-' in (model_num or '').upper():
|
||||
key = 'induction_cooktop'
|
||||
# Stick-vacuum clean/auto-empty station (e.g. A-VSKR-TP1-22-VS9500AL,
|
||||
# issue #131) -- reports no oneUiVersion and carries the '-VSKR-'
|
||||
# board-family token. See capabilities/vacuum_station.py for why this
|
||||
# is its own device type: the resource set (dustbag/dustbin/UV-sanitize
|
||||
# station state) shares no hrefs with anything else already modeled.
|
||||
if key is None and '-VSKR-' in (model_num or '').upper():
|
||||
key = 'vacuum_station'
|
||||
# Consumer-model prefix from `description` (washer/dryer/dishwasher) --
|
||||
# last, since it's the fuzziest match (a bare 2-letter prefix) and would
|
||||
# otherwise shadow the more specific board-family tokens above.
|
||||
if key is None:
|
||||
key = _consumer_model_key(description)
|
||||
return _REGISTRY_BY_KEY.get(key) if key else None
|
||||
|
||||
|
||||
@@ -170,4 +292,18 @@ def for_device_by_resources(resources: dict[str, dict]) -> Optional[DeviceRegist
|
||||
and '/hood/lamp/vs/0' in resources
|
||||
):
|
||||
return _REGISTRY_BY_KEY['range_hood']
|
||||
# Oven/range-combo boards that report no /information/vs/0 at all
|
||||
# (issue #74's NE63B8411SS -- the resource is simply absent from the
|
||||
# dump, not just empty) can't be matched via for_device_by_model's
|
||||
# modelNum tokens either. 'Bake' is oven/range cook-mode vocabulary that
|
||||
# no other family's /mode/vs/0 uses (confirmed against the microwave,
|
||||
# cooktop, and every laundry fixture), so its presence alongside the
|
||||
# oven cavity resource is a safe signature. Distinguish range (has a
|
||||
# cooktop half) from a plain wall oven by which cooktop-status resource,
|
||||
# if any, is also present.
|
||||
supported_modes = mode.get('x.com.samsung.da.supportedModes') or ()
|
||||
if '/oven/vs/0' in resources and 'Bake' in supported_modes:
|
||||
if '/cooktopmonitoring/vs/0' in resources or '/cooktop/status/vs/0' in resources:
|
||||
return _REGISTRY_BY_KEY['range']
|
||||
return _REGISTRY_BY_KEY['oven']
|
||||
return None
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
"""Air-purifier device registry (Samsung ARTIK051_TVTL-class, issue #56).
|
||||
"""Air-purifier device registry.
|
||||
|
||||
Reports no oneUiVersion; resolved via for_device_by_model's '_TVTL_' modelNum
|
||||
token (see registry.py). Reuses dishwasher.DIAGNOSIS for /diagnosis/vs/0
|
||||
(identical field/write contract).
|
||||
Spans two board generations sharing this one registry (see
|
||||
capabilities/air_purifier.py's module docstring for the per-href
|
||||
match_fn discriminators that keep them from colliding):
|
||||
|
||||
- ARTIK051_TVTL-class (issue #56). Reports no oneUiVersion; resolved via
|
||||
for_device_by_model's '_TVTL_' modelNum token (see registry.py).
|
||||
- TP1X_DA-AC-AIR-class (issue #130). Self-reports oneUiVersion "7.0 Air
|
||||
purifier", resolved via for_device(). Adds real fan-mode control plus
|
||||
display/HEPA-filter/pet-filter/sound resources the older family never
|
||||
reported; reuses airconditioner.DISPLAY_LIGHT and airconditioner.MUTE_ONCE
|
||||
for /light/vs/0 and /option/muteonce/vs/0, which are identical shapes on
|
||||
the shared DA-AC- board family.
|
||||
|
||||
Reuses dishwasher.DIAGNOSIS for /diagnosis/vs/0 (identical field/write
|
||||
contract).
|
||||
"""
|
||||
from ..capabilities import air_purifier, common, dishwasher, ignored
|
||||
from ..capabilities import air_purifier, airconditioner, common, dishwasher, ignored
|
||||
from ._base import DeviceRegistry, _build
|
||||
|
||||
REGISTRY = DeviceRegistry(
|
||||
@@ -20,6 +32,16 @@ REGISTRY = DeviceRegistry(
|
||||
air_purifier.AIRFLOW_GENERIC,
|
||||
air_purifier.AIRFLOW_VS_FALLBACK,
|
||||
air_purifier.MODE,
|
||||
air_purifier.FAN,
|
||||
air_purifier.DISPLAY,
|
||||
air_purifier.HEPA_FILTER,
|
||||
air_purifier.PANEL_STATUS,
|
||||
air_purifier.PET_FILTER_ACTIVATION,
|
||||
air_purifier.SOUND_MODE,
|
||||
air_purifier.SOUND_OUTPUT,
|
||||
air_purifier.SOUND_VOLUME,
|
||||
airconditioner.DISPLAY_LIGHT,
|
||||
airconditioner.MUTE_ONCE,
|
||||
*air_purifier.COVERAGE,
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -25,6 +25,10 @@ REGISTRY = DeviceRegistry(
|
||||
airconditioner.DISPLAY_LIGHT,
|
||||
airconditioner.MUTE_ONCE,
|
||||
airconditioner.CURRENT_LIMIT,
|
||||
airconditioner.ANOMALY_LOAD,
|
||||
airconditioner.CURRENT_TEMPERATURE,
|
||||
airconditioner.CURRENT_TEMPERATURE_VS,
|
||||
airconditioner.HUMIDITY,
|
||||
*airconditioner.COVERAGE,
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
"""Cooktop device registry."""
|
||||
"""Gas cooktop device registry (NA9300K-class, PR #23).
|
||||
|
||||
Named 'gas_cooktop' (not 'cooktop') so its diagnostics/device-info label
|
||||
doesn't collide with the unrelated induction_cooktop family (issue #86,
|
||||
by_type/induction_cooktop.py) -- two different OCF surfaces that happen to
|
||||
share the English word "cooktop". The `_REGISTRY_BY_KEY['cooktop']` lookup
|
||||
key is unchanged: it's relied on by real devices reporting
|
||||
oneUiVersion "Cooktop" (for_device), the legacy ARTIK051 modelNum rule
|
||||
(for_device_by_model), and the resource-signature fallback
|
||||
(for_device_by_resources) alike.
|
||||
"""
|
||||
|
||||
from ..capabilities import common, cooktop, ignored
|
||||
from ._base import DeviceRegistry, _build
|
||||
|
||||
|
||||
REGISTRY = DeviceRegistry(
|
||||
name='cooktop',
|
||||
name='gas_cooktop',
|
||||
capabilities=_build([
|
||||
*ignored.IGNORED,
|
||||
cooktop.COOKTOP_POWER,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Dehumidifier device registry (Samsung TP1X_DA_AC_DHM-class, issue #88).
|
||||
|
||||
Shares the DA_AC_ board family with airconditioner.py (power, air filter,
|
||||
auto-clean, mute-once all use the identical resource shapes), so those three
|
||||
Capability objects are reused directly rather than duplicated.
|
||||
"""
|
||||
from ..capabilities import airconditioner, common, dehumidifier, ignored
|
||||
from ._base import DeviceRegistry, _build
|
||||
|
||||
REGISTRY = DeviceRegistry(
|
||||
name='dehumidifier',
|
||||
capabilities=_build([
|
||||
*ignored.IGNORED,
|
||||
*common.UNIVERSAL,
|
||||
*common.POWER,
|
||||
dehumidifier.MODE,
|
||||
dehumidifier.HUMIDITY,
|
||||
airconditioner.AUTO_CLEAN,
|
||||
airconditioner.AIR_FILTER,
|
||||
airconditioner.MUTE_ONCE,
|
||||
*dehumidifier.COVERAGE,
|
||||
]),
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Standalone induction-cooktop device registry (Samsung
|
||||
TP1X_DA-KS-COOKTOP-class, issue #86) -- the cooktop half of the
|
||||
range/oven board family (see capabilities/range.py), but with no oven
|
||||
attached at all.
|
||||
|
||||
Reuses range.py's COOKTOP_STATUS/COOKTOP_SPEC/COOKTOP_SAFETY/PROBE_STATUS
|
||||
wholesale (identical resource shapes to the range combo's cooktop half)
|
||||
and cooktop.PAIRED_HOOD_STATUS for the Bluetooth-paired range hood some
|
||||
units pair with. Distinct registry key from cooktop.REGISTRY ('cooktop')
|
||||
-- that family is the unrelated NA9300K-class gas cooktop (burner state
|
||||
embedded in /mode/vs/0's options array, a completely different OCF
|
||||
surface that happens to share the English word "cooktop").
|
||||
"""
|
||||
from ..capabilities import common, cooktop as cooktop_caps, ignored
|
||||
from ..capabilities import range as range_caps
|
||||
from ._base import DeviceRegistry, _build
|
||||
|
||||
REGISTRY = DeviceRegistry(
|
||||
name='induction_cooktop',
|
||||
capabilities=_build([
|
||||
*ignored.IGNORED,
|
||||
*common.UNIVERSAL,
|
||||
*common.POWER,
|
||||
range_caps.COOKTOP_STATUS,
|
||||
range_caps.COOKTOP_SPEC,
|
||||
range_caps.COOKTOP_SAFETY,
|
||||
range_caps.PROBE_STATUS,
|
||||
cooktop_caps.PAIRED_HOOD_STATUS,
|
||||
]),
|
||||
)
|
||||
@@ -15,5 +15,6 @@ REGISTRY = DeviceRegistry(
|
||||
oven.OVEN_DOOR,
|
||||
oven.OVEN_CONNECTED,
|
||||
oven.OVEN_SPEC,
|
||||
oven.OVEN_RECIPE_COOK,
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -25,5 +25,6 @@ REGISTRY = DeviceRegistry(
|
||||
range_caps.COOKTOP_STATUS,
|
||||
range_caps.COOKTOP_SPEC,
|
||||
range_caps.COOKTOP_SAFETY,
|
||||
range_caps.COOKTOP_MONITORING,
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ REGISTRY = DeviceRegistry(
|
||||
]),
|
||||
pattern_capabilities=[
|
||||
fridge.TEMP_CURRENT_GENERIC,
|
||||
fridge.TEMP_SETPOINT_GENERIC,
|
||||
fridge.TEMP_SETPOINT,
|
||||
fridge.ICEMAKER_GENERIC,
|
||||
fridge.DOOR_GENERIC,
|
||||
],
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Stick-vacuum clean/auto-empty station device registry (issue #131).
|
||||
|
||||
See capabilities/vacuum_station.py's module docstring for why this only
|
||||
covers the station's own dustbag/dustbin/UV-sanitize state and not any
|
||||
vacuum-body control (suction, battery, cleaning mode) -- the diagnostics
|
||||
dump this was built from reports none of that.
|
||||
"""
|
||||
from ..capabilities import common, ignored, vacuum_station
|
||||
from ._base import DeviceRegistry, _build
|
||||
|
||||
REGISTRY = DeviceRegistry(
|
||||
name='vacuum_station',
|
||||
capabilities=_build([
|
||||
*ignored.IGNORED,
|
||||
*common.UNIVERSAL,
|
||||
*common.POWER,
|
||||
vacuum_station.DUSTBAG,
|
||||
vacuum_station.DUSTBAG_USAGE,
|
||||
vacuum_station.DUSTBIN_SETTING,
|
||||
vacuum_station.CLEANSTATION_STATUS,
|
||||
]),
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Water-purifier device registry (Samsung TP2X_WATERPURIFIER-class, issue #90).
|
||||
"""
|
||||
from ..capabilities import common, ignored, water_purifier
|
||||
from ._base import DeviceRegistry, _build
|
||||
|
||||
REGISTRY = DeviceRegistry(
|
||||
name='water_purifier',
|
||||
capabilities=_build([
|
||||
*ignored.IGNORED,
|
||||
*common.UNIVERSAL,
|
||||
common.WATER_FILTER,
|
||||
water_purifier.DISPENSE,
|
||||
water_purifier.STATUS,
|
||||
water_purifier.FAVORITE_CAPACITY,
|
||||
water_purifier.FAVORITE_HOTWATER,
|
||||
water_purifier.COFFEE,
|
||||
water_purifier.LOCK,
|
||||
*water_purifier.COVERAGE,
|
||||
]),
|
||||
)
|
||||
@@ -15,6 +15,13 @@ issue #56's follow-up (five diagnostics dumps captured with the physical unit
|
||||
set to Auto/Sleep/Low/Medium/High):
|
||||
Light_On / Light_Off -- a plain on/off flag; MODE below models it as a
|
||||
real switch, RMW-replacing just that one entry.
|
||||
NOT the same polarity as the AC family's own
|
||||
Light_On/Light_Off token on its own /mode/vs/0
|
||||
(airconditioner._display_light_on) -- that one is
|
||||
confirmed inverted (Light_Off means the panel is
|
||||
lit) on live hardware. Same token name, same
|
||||
resource name, different device type and
|
||||
opposite meaning -- don't unify them.
|
||||
Comode_Off -- read 'Off' on *every* one of the five dumps,
|
||||
including High/Low/Medium/Auto -- confirms this
|
||||
is NOT the fan-speed selector (ruling out the
|
||||
@@ -42,10 +49,26 @@ diagnostics snapshot was taken. Exposed read-only pending a confirmed,
|
||||
stable capture -- see the issue #56 discussion for what's needed.
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import BinarySensorDesc, SensorDesc, SwitchDesc
|
||||
from .common import int_or_none, sensor_item_value
|
||||
from ..entities import (
|
||||
BinarySensorDesc, FanDesc, NumberDesc, SelectDesc, SensorDesc, SwitchDesc,
|
||||
)
|
||||
from .common import filter_usage_percent, int_or_none, sensor_item_value
|
||||
from .laundry import bool_option_exists, bool_option_value, option_value, option_write
|
||||
|
||||
# Newer TP1X_DA-AC-AIR-class boards (e.g. TP1X_DA-AC-AIR-01031_0000, issue
|
||||
# #130) report fan modes directly on /mode/vs/0's top-level `modes`/
|
||||
# `supportedModes` fields (Smart/Max/Mid/WindFree/Sleep) instead of packing
|
||||
# everything into the options[] array the way the older ARTIK051_TVTL
|
||||
# family above does -- that older family's /mode/vs/0 has no top-level
|
||||
# supportedModes at all (see the module docstring's Comode_Off finding).
|
||||
# Both board generations share the /mode/vs/0 href, so FAN and MODE below
|
||||
# are mutually exclusive via this presence check rather than colliding.
|
||||
HREF_MODE = '/mode/vs/0'
|
||||
|
||||
|
||||
def _has_top_level_modes(rep, resources):
|
||||
return isinstance(rep.get('x.com.samsung.da.supportedModes'), (list, tuple))
|
||||
|
||||
_AIR_QUALITY_SENSORS = (
|
||||
('dust', 'mdi:blur', 'Dust'),
|
||||
('fine_dust', 'mdi:blur', 'FineDust'),
|
||||
@@ -149,6 +172,7 @@ def _light_write(payload, rep, href=None):
|
||||
MODE = Capability(
|
||||
href='/mode/vs/0',
|
||||
poll_tier='warm',
|
||||
match_fn=lambda rep, resources: not _has_top_level_modes(rep, resources),
|
||||
entities=(
|
||||
SwitchDesc(key='display_light', icon='mdi:led-on',
|
||||
entity_category='config',
|
||||
@@ -164,11 +188,200 @@ 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'})
|
||||
if kind == 'mode':
|
||||
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': [value]}
|
||||
return None
|
||||
|
||||
|
||||
def _first_fan_mode(rep):
|
||||
"""Representative scalar for the fan entity in the flattened state
|
||||
(golden/regression), mirroring airconditioner.py's own _first_mode --
|
||||
the real entity computes its state from live coordinator reads."""
|
||||
modes = rep.get('x.com.samsung.da.modes')
|
||||
if isinstance(modes, (list, tuple)):
|
||||
return modes[0] if modes else None
|
||||
return modes
|
||||
|
||||
|
||||
# Named preset modes (Smart/Max/Mid/WindFree/Sleep), not an ordered
|
||||
# percentage -- WindFree/Smart/Sleep are named behaviors, not
|
||||
# "faster/slower" positions relative to Max/Mid, so fan.py's entity for
|
||||
# this only exposes PRESET_MODE, matching how the AC family's own named
|
||||
# convenient modes are modeled as a preset rather than a speed number.
|
||||
FAN = Capability(
|
||||
href=HREF_MODE,
|
||||
poll_tier='warm',
|
||||
match_fn=_has_top_level_modes,
|
||||
entities=(
|
||||
FanDesc(key='fan', translation_key='air_purifier_fan',
|
||||
rep_fn=_first_fan_mode, write_fn=_fan_write),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TP1X_DA-AC-AIR-class additions (issue #130). This board reports several
|
||||
# resources the older ARTIK051_TVTL family never did.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Screen/indicator-panel on/off -- distinct from LIGHT below (ambient mood
|
||||
# light): both report the same {mode, supportedModes: [On, Off]} shape on
|
||||
# separate hrefs on this dump, so they're two independent physical controls,
|
||||
# not a duplicate encoding of one.
|
||||
DISPLAY = Capability(
|
||||
href='/display/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SwitchDesc(key='display', field='mode',
|
||||
icon='mdi:monitor',
|
||||
entity_category='config',
|
||||
value_fn=lambda v: v == 'On',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['display', 'vs', '0'],
|
||||
{'mode': 'On' if p == 'On' else 'Off'})),
|
||||
),
|
||||
)
|
||||
|
||||
# Same filterUsage/filterCapacity/filterStatus shape as the AC family's own
|
||||
# AIR_FILTER (airconditioner.py) -- confirmed normal/wash/replace values not
|
||||
# seen on this one dump, so the option list there is reused as-is rather
|
||||
# than re-deriving it from a single sample.
|
||||
HEPA_FILTER = Capability(
|
||||
href='/filter/hepafilter/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SensorDesc(key='hepa_filter_usage', rep_fn=filter_usage_percent,
|
||||
unit='%', state_class='measurement',
|
||||
icon='mdi:air-filter', entity_category='diagnostic'),
|
||||
SensorDesc(key='hepa_filter_status', field='x.com.samsung.da.filterStatus',
|
||||
device_class='enum',
|
||||
options=('normal', 'wash', 'replace'),
|
||||
translation_key='filter_status',
|
||||
icon='mdi:air-filter', entity_category='diagnostic',
|
||||
value_fn=lambda v: v.lower() if isinstance(v, str) else v),
|
||||
),
|
||||
)
|
||||
|
||||
# Physical panel/cover status -- meaning of the one value seen ('Close') is
|
||||
# plausible (the HEPA-filter access cover) but unconfirmed, and no
|
||||
# supportedStatus list is present to check against -- exposed as a plain
|
||||
# diagnostic sensor rather than an asserted binary_sensor polarity.
|
||||
PANEL_STATUS = Capability(
|
||||
href='/panel/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SensorDesc(key='panel_status', field='status',
|
||||
icon='mdi:archive-outline', entity_category='diagnostic'),
|
||||
),
|
||||
)
|
||||
|
||||
# Pet-care filter mode -- a plain On/Off field with no vendor prefix, same
|
||||
# convention as airconditioner.MUTE_ONCE.
|
||||
PET_FILTER_ACTIVATION = Capability(
|
||||
href='/petfilteractivation/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SwitchDesc(key='pet_filter_activation', field='status',
|
||||
icon='mdi:paw',
|
||||
entity_category='config',
|
||||
value_fn=lambda v: v == 'On',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['petfilteractivation', 'vs', '0'],
|
||||
{'status': 'On' if p == 'On' else 'Off'})),
|
||||
),
|
||||
)
|
||||
|
||||
# Sound mode/volume shapes look like laundry.py's SOUND_MODE/SOUND_VOLUME at
|
||||
# a glance, but this board's actual values differ (supportedModes here is
|
||||
# ['mute', 'buzzer'], not laundry's hardcoded voice/tone/mute; volume range
|
||||
# is 0-3, not laundry's fixed 0-15) -- reusing those would either reject a
|
||||
# valid write ('buzzer') or expose the wrong number range, so these are
|
||||
# separate descriptors reading the live supported values instead of a
|
||||
# hardcoded table.
|
||||
SOUND_MODE = Capability(
|
||||
href='/settings/sound/mode/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
# Distinct translation_key from laundry.SOUND_MODE's shared
|
||||
# 'sound_mode' catalog entry -- that one's state table is
|
||||
# {voice, tone, mute}, but this board's supportedModes is
|
||||
# {mute, buzzer}. Sharing the key would leave 'buzzer' unlabelled
|
||||
# (falls through to the raw code) since the catalogs don't overlap.
|
||||
SelectDesc(key='sound_mode', translation_key='air_purifier_sound_mode',
|
||||
field='mode',
|
||||
icon='mdi:volume-high',
|
||||
entity_category='config',
|
||||
options_field='supportedModes',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['settings', 'sound', 'mode', 'vs', '0'], {'mode': p})),
|
||||
),
|
||||
)
|
||||
|
||||
# Read-only descriptor of which sound output the unit has -- only one value
|
||||
# seen, no alternatives to select between.
|
||||
SOUND_OUTPUT = Capability(
|
||||
href='/settings/sound/output/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SensorDesc(key='sound_output', field='deviceType',
|
||||
icon='mdi:volume-high', entity_category='diagnostic'),
|
||||
),
|
||||
)
|
||||
|
||||
SOUND_VOLUME = Capability(
|
||||
href='/settings/sound/volume/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
NumberDesc(key='sound_volume', field='level',
|
||||
icon='mdi:volume-medium',
|
||||
entity_category='config',
|
||||
native_min_fn=lambda rep: int_or_none(rep.get('minLevel')) or 0,
|
||||
native_max_fn=lambda rep: int_or_none(rep.get('maxLevel')) or 0,
|
||||
step_fn=lambda rep: int_or_none(rep.get('resolution')) or 1,
|
||||
value_fn=int_or_none,
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['settings', 'sound', 'volume', 'vs', '0'],
|
||||
{'level': str(int(p))})),
|
||||
),
|
||||
)
|
||||
|
||||
# /humidity/0 and /humidity/vs/0 are empty {} on both dumps this family has
|
||||
# been verified against -- covered here (not globally, per ignored.py's
|
||||
# module docstring) since those hrefs collide with fridge/AC schemas
|
||||
# elsewhere. Same two hrefs and reasoning as airconditioner.py's _AC_IGNORED.
|
||||
#
|
||||
# The next six hrefs (issue #130, TP1X_DA-AC-AIR board) are the exact same
|
||||
# resources, same shapes, same reasoning as airconditioner.py's
|
||||
# _AC_IGNORED on the shared DA-AC- board family -- duplicated here rather
|
||||
# than promoted to the global ignored.py list, since that would require
|
||||
# also removing them from _AC_IGNORED in the same change (a global entry
|
||||
# colliding with a family-local bare Capability on the same href raises in
|
||||
# _build()); left as a possible follow-up DRY cleanup.
|
||||
COVERAGE = [
|
||||
Capability(href='/humidity/0'),
|
||||
Capability(href='/humidity/vs/0'),
|
||||
Capability(href='/airlevelcheck/vs/0'), # periodic air-quality sensing scheduler plumbing
|
||||
Capability(href='/availablecontrolsets/vs/0'), # opaque hex-encoded control-set bitmap
|
||||
Capability(href='/da/softreset/vs/0'), # soft-reset trigger plumbing
|
||||
Capability(href='/keepnormalstate/vs/0'), # internal keep-normal flag
|
||||
Capability(href='/personality/presence/vs/0'), # presence-personalization plumbing (empty here)
|
||||
Capability(href='/reserverulesets/vs/0'), # opaque hex-encoded schedule reservation blob
|
||||
# Do-not-disturb/auto-sleep schedule (visible/startTime/endTime/
|
||||
# useTimeSetting/functionState) -- every field reads its inert default
|
||||
# on the only dump seen (times both '00:00:00', useTimeSetting/
|
||||
# functionState both 'false'). Same "needs a multi-field schedule
|
||||
# editor" treatment as fridge.py's /defrost/reservation/vs/0.
|
||||
Capability(href='/dnd/autosleep/vs/0'),
|
||||
]
|
||||
|
||||
@@ -16,6 +16,8 @@ by_type registry.
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import BinarySensorDesc, ClimateDesc, SensorDesc, SwitchDesc
|
||||
from .common import filter_usage_percent, normalize_temp_unit
|
||||
from .laundry import option_write
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Canonical AC resource hrefs. The climate entity (climate.py) binds the
|
||||
@@ -33,13 +35,19 @@ HREF_TEMP_DESIRED = '/temperature/desired/0' # target_temperature (write ta
|
||||
HREF_TEMP_CONTROL = '/temperature/control/vs/0' # target_temperature_step
|
||||
HREF_WIND_STRENGTH = '/wind/strength/vs/0' # fan_mode
|
||||
HREF_WIND_DIRECTION = '/wind/direction/vs/0' # swing_mode
|
||||
# Newer boards (issue #126, TP1X_DA-AC-RAC-01011 WindFree variant) report no
|
||||
# HREF_WIND_DIRECTION at all and instead carry a 2-axis oscillation resource
|
||||
# (separate vertical/horizontal Swing|Fix toggles, each with its own angle).
|
||||
# climate.py falls back to this when HREF_WIND_DIRECTION is absent, the same
|
||||
# duality pattern as the OCF/vendor temperature channel below.
|
||||
HREF_WIND_OSCILLATION = '/wind/oscillation/vs/0' # swing_mode fallback
|
||||
HREF_CONVENIENT = '/mode/convenient/vs/0' # preset_mode
|
||||
HREF_TEMPS_VS = '/temperatures/vs/0' # vendor temp fallback (items[] array)
|
||||
|
||||
CLIMATE_CONSUMED_HREFS = [
|
||||
HREF_POWER, HREF_POWER_VS, HREF_TEMP_CURRENT, HREF_TEMP_DESIRED,
|
||||
HREF_TEMP_CONTROL, HREF_TEMPS_VS, HREF_WIND_STRENGTH, HREF_WIND_DIRECTION,
|
||||
HREF_CONVENIENT,
|
||||
HREF_WIND_OSCILLATION, HREF_CONVENIENT,
|
||||
]
|
||||
|
||||
|
||||
@@ -50,16 +58,23 @@ def _num(v):
|
||||
return None
|
||||
|
||||
|
||||
def _filter_usage_percent(rep):
|
||||
"""Filter usage as a percentage of rated capacity. The device reports
|
||||
`filterUsage` as a raw count in `filterCapacityUnit` (Hours here, e.g.
|
||||
100 of a 500 capacity), so a plain value with a '%' unit would be wrong --
|
||||
normalize to used/capacity. Returns None when capacity is missing/zero."""
|
||||
used = _num(rep.get('x.com.samsung.da.filterUsage'))
|
||||
cap = _num(rep.get('x.com.samsung.da.filterCapacity'))
|
||||
if used is None or not cap:
|
||||
return None
|
||||
return round(used / cap * 100)
|
||||
def _temps_vs_item(rep):
|
||||
"""First item of the vendor `/temperatures/vs/0` items[] array -- the
|
||||
Tizen Lite board's only current-temperature source (see climate.py's
|
||||
identical helper; duplicated rather than imported to avoid a
|
||||
capabilities<->platform import cycle)."""
|
||||
items = rep.get('x.com.samsung.da.items')
|
||||
if isinstance(items, (list, tuple)) and items and isinstance(items[0], dict):
|
||||
return items[0]
|
||||
return {}
|
||||
|
||||
|
||||
def _temps_vs_current(rep):
|
||||
return _num(_temps_vs_item(rep).get('x.com.samsung.da.current'))
|
||||
|
||||
|
||||
def _temps_vs_unit(rep):
|
||||
return normalize_temp_unit(_temps_vs_item(rep).get('x.com.samsung.da.unit'), '°C')
|
||||
|
||||
|
||||
def _first_mode(rep):
|
||||
@@ -71,25 +86,100 @@ def _first_mode(rep):
|
||||
return modes
|
||||
|
||||
|
||||
def _mode_options(rep):
|
||||
opts = rep.get('x.com.samsung.da.options')
|
||||
return opts if isinstance(opts, (list, tuple)) else ()
|
||||
|
||||
|
||||
def _has_display_light_option(rep, resources):
|
||||
"""True when the panel light state is carried inside /mode/vs/0's options
|
||||
blob (a `Light_On`/`Light_Off` token) rather than a dedicated /light/vs/0
|
||||
switch. The two encodings are mutually exclusive across observed boards:
|
||||
models exposing the /light/vs/0 switch (bound by DISPLAY_LIGHT below)
|
||||
carry no Light_* option, so this entity only materialises on the boards
|
||||
that would otherwise have no display-light entity at all."""
|
||||
return any(isinstance(o, str) and o.startswith('Light_')
|
||||
for o in _mode_options(rep))
|
||||
|
||||
|
||||
def _display_light_on(rep):
|
||||
"""Panel display light state from /mode/vs/0's options blob. The token is
|
||||
INVERTED relative to its name -- confirmed by a live toggle test: with the
|
||||
panel lit the option reads `Light_Off`, and with it dark it reads
|
||||
`Light_On` (the flag really encodes "night/display-off mode active"). So
|
||||
`Light_Off` -> light on, `Light_On` -> light off. Read-only from here; the
|
||||
write below uses the device's own single-token merge (see option_write)."""
|
||||
for o in _mode_options(rep):
|
||||
if isinstance(o, str) and o.startswith('Light_'):
|
||||
return o == 'Light_Off'
|
||||
return None
|
||||
|
||||
|
||||
def _display_light_write(payload, rep, href=None):
|
||||
"""Toggle the panel light via a single-token /mode/vs/0 options write
|
||||
('SingleCommand_1' is advertised in this family's /configuration/vs/0
|
||||
airconOptionList; option_write's one-token merge is the same mechanism
|
||||
air_purifier.py uses for its own Light switch). Polarity is inverted (see
|
||||
_display_light_on): switching the lamp ON writes 'Light_Off', OFF writes
|
||||
'Light_On'."""
|
||||
token = 'Off' if payload == 'On' else 'On'
|
||||
return (['mode', 'vs', '0'],
|
||||
{'x.com.samsung.da.options': option_write('Light', token)})
|
||||
|
||||
|
||||
def _climate_write(payload, rep, href=None):
|
||||
"""Map a (kind, value) command from the climate platform to the
|
||||
(path_segs, body) for that one sub-write. `value` is already the raw device
|
||||
code (the platform maps HA<->device). async_send_command POSTs to path_segs,
|
||||
so a single desc drives writes across power/mode/temperature/wind resources.
|
||||
Read-modify-write safe: each write sends only its own field, leaving the
|
||||
resource's other fields (e.g. /mode/vs/0's opaque `options` blob) untouched.
|
||||
so a single desc drives writes across the power/mode/temperature/wind
|
||||
resources.
|
||||
|
||||
Power goes to the vendor `/power/vs/0` (the OCF `/power/0` is absent on
|
||||
most boards and a non-authoritative mirror where present -- vendor works
|
||||
on every board). Temperature is board-dependent, so the platform picks the
|
||||
channel and sends `temperature_ocf` (-> OCF `/temperature/desired/0`,
|
||||
boards with the full OCF current+desired pair) or `temperature` (-> vendor
|
||||
`/temperatures/vs/0`, boards without it). Mode/fan/swing/preset are always
|
||||
the vendor `/x/vs/0` resources.
|
||||
|
||||
Each write sends only its own field(s), leaving the resource's other
|
||||
fields (e.g. /mode/vs/0's opaque `options` blob, or the vendor temperature
|
||||
item's current/minimum/maximum/unit) untouched -- the device merges the
|
||||
rest itself, same contract as the options[] array (see
|
||||
common.merge_items_field / merge_options_field, which keep the
|
||||
coordinator's optimistic cache complete for the settle window instead of
|
||||
this write echoing those fields back).
|
||||
"""
|
||||
kind, value = payload
|
||||
if kind == 'power':
|
||||
return (['power', '0'], {'value': bool(value)})
|
||||
return (['power', 'vs', '0'],
|
||||
{'x.com.samsung.da.power': 'On' if value else 'Off'})
|
||||
if kind == 'mode':
|
||||
return (['mode', 'vs', '0'], {'x.com.samsung.da.modes': [value]})
|
||||
if kind == 'temperature_ocf':
|
||||
return (['temperature', 'desired', '0'],
|
||||
{'temperature': int(round(float(value)))})
|
||||
if kind == 'temperature':
|
||||
return (['temperature', 'desired', '0'], {'temperature': int(round(float(value)))})
|
||||
# Vendor items[] array; only one item observed on every AC dump, id
|
||||
# '0'. See the docstring above for why this doesn't echo current/
|
||||
# minimum/maximum/unit back at the unit.
|
||||
return (['temperatures', 'vs', '0'],
|
||||
{'x.com.samsung.da.items': [
|
||||
{'x.com.samsung.da.id': '0',
|
||||
'x.com.samsung.da.desired': str(int(round(float(value))))}]})
|
||||
if kind == 'fan':
|
||||
return (['wind', 'strength', 'vs', '0'], {'x.com.samsung.da.modes': value})
|
||||
if kind == 'swing':
|
||||
return (['wind', 'direction', 'vs', '0'], {'x.com.samsung.da.modes': value})
|
||||
if kind == 'oscillation':
|
||||
# value is the HA swing_mode string ('off'/'vertical'/'horizontal'/
|
||||
# 'both'); both axes are independent Swing|Fix toggles on this
|
||||
# resource, so one HA value maps to a pair of fields written
|
||||
# together (see climate.py's oscillation fallback).
|
||||
return (['wind', 'oscillation', 'vs', '0'], {
|
||||
'vertical': 'Swing' if value in ('vertical', 'both') else 'Fix',
|
||||
'horizontal': 'Swing' if value in ('horizontal', 'both') else 'Fix',
|
||||
})
|
||||
if kind == 'preset':
|
||||
return (['mode', 'convenient', 'vs', '0'], {'x.com.samsung.da.modes': value})
|
||||
return None
|
||||
@@ -101,6 +191,19 @@ CLIMATE = Capability(
|
||||
entities=(
|
||||
ClimateDesc(key='climate', translation_key='airconditioner',
|
||||
rep_fn=_first_mode, write_fn=_climate_write),
|
||||
# Display (panel) light switch, only on boards that encode it in
|
||||
# /mode/vs/0's options instead of a /light/vs/0 switch (see
|
||||
# _has_display_light_option). Shares the /mode/vs/0 href with the
|
||||
# climate entity above -- same Capability, so no multi-cap
|
||||
# discriminator is needed. /mode/vs/0 is OBSERVE-subscribed, so state
|
||||
# updates on push; writes go through the single-token merge in
|
||||
# _display_light_write. Shares the switch.display_light translation
|
||||
# with DISPLAY_LIGHT (the /light/vs/0 switch on other boards) --
|
||||
# mutually exclusive per href, so only one ever binds for a given unit.
|
||||
SwitchDesc(key='display_light', rep_fn=_display_light_on,
|
||||
exists_fn=_has_display_light_option,
|
||||
write_fn=_display_light_write,
|
||||
icon='mdi:led-on', entity_category='config'),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -136,7 +239,7 @@ AIR_FILTER = Capability(
|
||||
href='/filter/airdustfilter/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SensorDesc(key='air_filter_usage', rep_fn=_filter_usage_percent,
|
||||
SensorDesc(key='air_filter_usage', rep_fn=filter_usage_percent,
|
||||
unit='%', state_class='measurement',
|
||||
icon='mdi:air-filter', entity_category='diagnostic'),
|
||||
SensorDesc(key='air_filter_status', field='x.com.samsung.da.filterStatus',
|
||||
@@ -199,6 +302,73 @@ CURRENT_LIMIT = Capability(
|
||||
),
|
||||
)
|
||||
|
||||
# Overload-response setting (issue #126, TP1X_DA-AC-RAC-01011 WindFree
|
||||
# variant): `operation` toggles the feature and `mode` picks 'Alarm' vs
|
||||
# 'PowerSaving' out of `supportedModes`, with a `savingTime` duration
|
||||
# ('20'/'40'/'60' minutes) alongside. Plausible shape (compressor-overload
|
||||
# alarm vs. automatic power throttling), but nothing in the dump confirms
|
||||
# the exact behavioral difference between the two modes or whether writing
|
||||
# 'operation' is safe on live HVAC hardware -- exposed read-only per the
|
||||
# 'don't guess' rule, same precedent as CURRENT_LIMIT above.
|
||||
ANOMALY_LOAD = Capability(
|
||||
href='/anomalyload/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
BinarySensorDesc(key='overload_protection_active', field='operation',
|
||||
icon='mdi:flash-alert',
|
||||
entity_category='diagnostic',
|
||||
value_fn=lambda v: v == 'On'),
|
||||
SensorDesc(key='overload_protection_mode', field='mode',
|
||||
device_class='enum',
|
||||
options=('alarm', 'powersaving'),
|
||||
translation_key='overload_protection_mode',
|
||||
icon='mdi:flash-alert', entity_category='diagnostic',
|
||||
value_fn=lambda v: v.lower() if isinstance(v, str) else v),
|
||||
),
|
||||
)
|
||||
|
||||
# The climate entity already surfaces current_temperature as a card
|
||||
# attribute, but that's not enough for history graphs/automations/
|
||||
# statistics -- issue #75 asked for a standalone sensor. Same OCF-standard-
|
||||
# with-vendor-fallback shape as common.py's POWER_GENERIC/POWER_VS_FALLBACK
|
||||
# pair: both share key='current_temperature_c' so only one ever binds
|
||||
# (match_fn gates the vendor one off when the OCF resource is present).
|
||||
CURRENT_TEMPERATURE = Capability(
|
||||
href=HREF_TEMP_CURRENT,
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SensorDesc(key='current_temperature_c', field='temperature',
|
||||
device_class='temperature', state_class='measurement',
|
||||
unit_fn=lambda rep: normalize_temp_unit(rep.get('units'), '°C')),
|
||||
),
|
||||
)
|
||||
|
||||
CURRENT_TEMPERATURE_VS = Capability(
|
||||
href=HREF_TEMPS_VS,
|
||||
poll_tier='warm',
|
||||
match_fn=lambda rep, resources: HREF_TEMP_CURRENT not in resources,
|
||||
entities=(
|
||||
SensorDesc(key='current_temperature_c', rep_fn=_temps_vs_current,
|
||||
device_class='temperature', state_class='measurement',
|
||||
unit_fn=_temps_vs_unit),
|
||||
),
|
||||
)
|
||||
|
||||
# Only the vendor resource's `fivepercentHumidity` (current reading, rounded
|
||||
# to the nearest 5%) has live data on the issue #75 dump -- its `humidity`
|
||||
# field, and the OCF-standard /humidity/0 resource entirely, both read a
|
||||
# stuck "0" there and stay ignored per the 'don't guess' rule (see
|
||||
# _AC_IGNORED below).
|
||||
HUMIDITY = Capability(
|
||||
href='/humidity/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SensorDesc(key='humidity', field='x.com.samsung.da.fivepercentHumidity',
|
||||
device_class='humidity', state_class='measurement', unit='%',
|
||||
value_fn=_num),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AC-scoped coverage: the CLIMATE_CONSUMED_HREFS above (read by the climate
|
||||
# entity) plus vendor duplicates / all-zero-ambiguous / plumbing resources.
|
||||
@@ -221,8 +391,10 @@ _AC_IGNORED = [
|
||||
# All-zero and ambiguously encoded on this model (2-value arrays); the
|
||||
# 'don't guess' rule -- leave unmodeled rather than invent entities.
|
||||
'/sensors/vs/0',
|
||||
# Stuck at "0" on every dump seen -- HUMIDITY above reads the vendor
|
||||
# resource's usable fivepercentHumidity field instead; this OCF-standard
|
||||
# one has no corresponding live value confirmed yet.
|
||||
'/humidity/0',
|
||||
'/humidity/vs/0',
|
||||
# Presence-personalization plumbing (empty item list here).
|
||||
'/personality/presence/vs/0',
|
||||
# --- TP1X/TP2X-class housekeeping / opaque blobs. These carry no
|
||||
@@ -240,7 +412,14 @@ _AC_IGNORED = [
|
||||
'/mds/absencemonitoring/vs/0', # motion-detection sensor plumbing (empty here)
|
||||
'/mds/absencestate/vs/0', # motion-detection state (empty here)
|
||||
'/remotedatacontrol/vs/0', # remote data-control session status
|
||||
'/remotedeviceinfo/vs/0', # remote paired-device id list (empty didList here)
|
||||
'/remotetemperature/vs/0', # external temp-sensor feed (unset on this unit)
|
||||
# Manual airflow-step position (supportedModes Off/80/60/40/Power).
|
||||
# Overlaps the /wind/direction swing control already on the climate card,
|
||||
# and the meaning of the numeric steps vs. 'Power' isn't documented in the
|
||||
# dump -- ignored per the 'don't guess' rule rather than modeled as a
|
||||
# select whose write could confuse live HVAC hardware.
|
||||
'/stepcontrol/vs/0',
|
||||
'/reserverulesets/vs/0', # opaque hex-encoded schedule reservation blob
|
||||
'/welcome/temperature/vs/0', # welcome-cooling plumbing
|
||||
# System-AC-only (multi-indoor-unit commercial installs, e.g.
|
||||
@@ -251,8 +430,11 @@ _AC_IGNORED = [
|
||||
]
|
||||
|
||||
# Built as bare no-entity caps; folded into the AC registry (not global).
|
||||
# HREF_TEMP_CURRENT and HREF_TEMPS_VS are excluded here -- CURRENT_TEMPERATURE
|
||||
# / CURRENT_TEMPERATURE_VS above already cover those two with real entities.
|
||||
COVERAGE = [
|
||||
Capability(href=h, poll_tier='warm') for h in CLIMATE_CONSUMED_HREFS
|
||||
if h not in (HREF_TEMP_CURRENT, HREF_TEMPS_VS)
|
||||
] + [
|
||||
Capability(href=h) for h in _AC_IGNORED
|
||||
]
|
||||
|
||||
@@ -10,6 +10,8 @@ against live device dumps:
|
||||
/water/consumption/vs/0 -> x.com.samsung.da.cumulativeWater
|
||||
/filter/waterfilter/vs/0 -> x.com.samsung.da.filterUsage / filterStatus
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from ..capability import Capability
|
||||
from ..entities import (
|
||||
BinarySensorDesc, ButtonDesc, SelectDesc, SensorDesc, SwitchDesc,
|
||||
@@ -40,6 +42,35 @@ def wh_to_kwh(v):
|
||||
return round(n / 1000.0, 2) if n is not None else None
|
||||
|
||||
|
||||
def parse_iso_utc(raw):
|
||||
"""ISO datetime defaulting to UTC when the string carries no timezone
|
||||
of its own (this integration's convention for other bare ISO datetime
|
||||
fields -- see washer.py's drum-clean-log comment). A few boards do
|
||||
ship a 'Z'/offset suffix (fromisoformat parses that natively since
|
||||
Python 3.11) -- only fill in UTC when parsing left the result naive,
|
||||
rather than unconditionally overwriting whatever offset was parsed."""
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
dt = datetime.fromisoformat(raw)
|
||||
except ValueError:
|
||||
return None
|
||||
return dt if dt.tzinfo is not None else dt.replace(tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def filter_usage_percent(rep):
|
||||
"""Filter usage as a percentage of rated capacity. Several families
|
||||
(AC, air purifier) report `filterUsage` as a raw count in
|
||||
`filterCapacityUnit` (Hours, e.g. 100 of a 500 capacity), so a plain
|
||||
value with a '%' unit would be wrong -- normalize to used/capacity.
|
||||
Returns None when capacity is missing/zero."""
|
||||
used = _num(rep.get('x.com.samsung.da.filterUsage'))
|
||||
cap = _num(rep.get('x.com.samsung.da.filterCapacity'))
|
||||
if used is None or not cap:
|
||||
return None
|
||||
return round(used / cap * 100)
|
||||
|
||||
|
||||
def normalize_temp_unit(raw, default='°F'):
|
||||
"""'C'/'Celsius' -> '°C', 'F'/'Fahrenheit' -> '°F'. Falls back to
|
||||
`default` for any other/missing value. Shared by fridge.py and oven.py,
|
||||
@@ -59,9 +90,21 @@ def _ml_to_l(v):
|
||||
|
||||
|
||||
def _active_alarm_codes(items):
|
||||
"""Join active alarm codes; skip retained rows Samsung leaves as Deleted.
|
||||
|
||||
Laundry boards keep a Deleted ErrorCode row in /alarms/vs/0 after the
|
||||
condition clears (see WD7000B diagnostics). Surface only live alarms so
|
||||
HA doesn't stick on a stale ErrorCode. Range-hood has its own stricter
|
||||
helper that also drops ErrorCode_OFF.
|
||||
"""
|
||||
if not items or not isinstance(items, list):
|
||||
return 'none'
|
||||
codes = [i.get('x.com.samsung.da.code') for i in items if i.get('x.com.samsung.da.code')]
|
||||
codes = [
|
||||
i.get('x.com.samsung.da.code')
|
||||
for i in items
|
||||
if i.get('x.com.samsung.da.code')
|
||||
and str(i.get('x.com.samsung.da.state', '')).lower() != 'deleted'
|
||||
]
|
||||
return ', '.join(codes) if codes else 'none'
|
||||
|
||||
|
||||
@@ -92,6 +135,80 @@ def merge_options_field(cached, new_tokens):
|
||||
return merged
|
||||
|
||||
|
||||
def merge_items_field(cached, new_items):
|
||||
"""Merge a partial x.com.samsung.da.items[]-style write (matched by
|
||||
x.com.samsung.da.id) into a cached items array -- the read-side
|
||||
counterpart of merge_options_field above, for the items[] shape instead
|
||||
of the packed options[] shape.
|
||||
|
||||
Confirmed on hardware that a write only needs to carry the array item
|
||||
with the changed id plus the field(s) being changed; the device merges
|
||||
the rest itself (same fact as the options[] case, different array --
|
||||
see airconditioner._climate_write's vendor temperature write). Fields
|
||||
within the matched item are merged, not replaced outright, so a
|
||||
setpoint-only write doesn't wipe current/minimum/maximum/unit from the
|
||||
optimistic cache entry for the settle window. An id with no match in
|
||||
`cached` is appended."""
|
||||
merged = [dict(i) if isinstance(i, dict) else i for i in (cached or [])]
|
||||
for new_item in new_items or ():
|
||||
if not isinstance(new_item, dict):
|
||||
continue
|
||||
item_id = new_item.get('x.com.samsung.da.id')
|
||||
for i, existing in enumerate(merged):
|
||||
if isinstance(existing, dict) and existing.get('x.com.samsung.da.id') == item_id:
|
||||
merged[i] = {**existing, **new_item}
|
||||
break
|
||||
else:
|
||||
merged.append(new_item)
|
||||
return merged
|
||||
|
||||
|
||||
# /wm/setinfo/vs/0 -- laundry-family firmware capability flags. Present on
|
||||
# washers, dryers, and dishwashers; absent on fridge/oven/AC. Static for the
|
||||
# life of a given board, so reading them from the /device/0 seed (no dedicated
|
||||
# poll_tier) is enough.
|
||||
_SETINFO_HREF = '/wm/setinfo/vs/0'
|
||||
_POWER_ON_OFF_FIELD = 'x.com.samsung.da.isModelSettingPowerOnOff'
|
||||
_WITHOUT_SC_FIELD = 'x.com.samsung.da.isModelSettingWithoutSC'
|
||||
|
||||
|
||||
def model_allows_power_on_off(resources: dict) -> bool:
|
||||
"""True unless firmware explicitly declares remote power on/off unsupported.
|
||||
|
||||
`/wm/setinfo/vs/0`.`isModelSettingPowerOnOff` is `"false"` on many laundry
|
||||
boards (washers/dryers): `/power/0` and `/power/vs/0` still report state,
|
||||
but CoAP writes are ignored. Absent setinfo (non-laundry families) keeps
|
||||
the writable switch -- current behavior.
|
||||
"""
|
||||
setinfo = resources.get(_SETINFO_HREF)
|
||||
if setinfo is None:
|
||||
return True
|
||||
flag = setinfo.get(_POWER_ON_OFF_FIELD)
|
||||
if flag is None:
|
||||
return True
|
||||
return str(flag).lower() != 'false'
|
||||
|
||||
|
||||
def model_setting_without_sc(resources: dict) -> bool:
|
||||
"""True when firmware declares settings writable without Smart Control.
|
||||
|
||||
`/wm/setinfo/vs/0`.`isModelSettingWithoutSC` is `"true"` on washers/dryers
|
||||
that accept temperature/spin/cycle-option writes while remote control is
|
||||
off. Cycle start/pause/stop still need Smart Control on those boards --
|
||||
the flag name is settings-specific, not a blanket remote-control bypass.
|
||||
"""
|
||||
setinfo = resources.get(_SETINFO_HREF) or {}
|
||||
return str(setinfo.get(_WITHOUT_SC_FIELD, '')).lower() == 'true'
|
||||
|
||||
|
||||
def _power_switch_exists(rep, resources):
|
||||
return model_allows_power_on_off(resources)
|
||||
|
||||
|
||||
def _power_sensor_exists(rep, resources):
|
||||
return not model_allows_power_on_off(resources)
|
||||
|
||||
|
||||
def sensor_item_value(items, sensor_type, index=0):
|
||||
"""Pull one reading out of a `/sensors/vs/0`-style items[] list -- each
|
||||
item is `{type, value: [...]}`; `index` picks which slot of a possibly
|
||||
@@ -128,10 +245,17 @@ def sensor_item_value(items, sensor_type, index=0):
|
||||
POWER_GENERIC = Capability(
|
||||
href='/power/0',
|
||||
entities=(
|
||||
# Writable when firmware allows remote power; otherwise a read-only
|
||||
# binary_sensor with the same key keeps HA state without a dead switch.
|
||||
SwitchDesc(key='power_switch', field='value',
|
||||
value_fn=lambda v: bool(v),
|
||||
exists_fn=_power_switch_exists,
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['power', '0'], {'value': p == 'On'})),
|
||||
BinarySensorDesc(key='power_switch', field='value',
|
||||
device_class='power',
|
||||
value_fn=lambda v: bool(v),
|
||||
exists_fn=_power_sensor_exists),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -141,9 +265,14 @@ POWER_VS_FALLBACK = Capability(
|
||||
entities=(
|
||||
SwitchDesc(key='power_switch', field='x.com.samsung.da.power',
|
||||
value_fn=lambda v: v == 'On',
|
||||
exists_fn=_power_switch_exists,
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['power', 'vs', '0'],
|
||||
{'x.com.samsung.da.power': 'On' if p == 'On' else 'Off'})),
|
||||
BinarySensorDesc(key='power_switch', field='x.com.samsung.da.power',
|
||||
device_class='power',
|
||||
value_fn=lambda v: v == 'On',
|
||||
exists_fn=_power_sensor_exists),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -192,6 +321,20 @@ def remote_control_enabled(resources: dict) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def remote_control_required_for_write(resources: dict, bound_href: str) -> bool:
|
||||
"""Whether a write to bound_href should be gated on Smart Control.
|
||||
|
||||
When isModelSettingWithoutSC is true, laundry firmware accepts settings
|
||||
writes (wash temp, spin, course options, buzzer, ...) with remote
|
||||
control off, but cycle start/pause/stop on /operational/state still
|
||||
need Smart Control. Absent that flag, keep the historical blanket gate.
|
||||
"""
|
||||
if not model_setting_without_sc(resources):
|
||||
return True
|
||||
href = bound_href or ''
|
||||
return href.startswith('/operational/state')
|
||||
|
||||
|
||||
REMOTE_CONTROL_GENERIC = Capability(
|
||||
href='/remotectrl/0',
|
||||
poll_tier='warm',
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Capabilities for the Samsung dehumidifier family (TP1X_DA_AC_DHM-class,
|
||||
issue #88, model AY18CG7500GED).
|
||||
|
||||
Same DA_AC_ board family as the room-AC models in airconditioner.py (shared
|
||||
power/energy/filter/auto-clean/mute-once resource shapes), but target
|
||||
humidity -- not temperature -- is this device's primary control, and there
|
||||
is no climate composite: power, mode, and humidity are exposed as separate
|
||||
entities rather than folded into one card.
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import NumberDesc, SelectDesc, SensorDesc
|
||||
from .common import int_or_none
|
||||
|
||||
|
||||
def _first_mode(rep):
|
||||
"""Representative scalar for the operating-mode select. `modes` is a
|
||||
single-element list on every dump seen so far, mirroring
|
||||
airconditioner._first_mode's handling of the same field shape."""
|
||||
modes = rep.get('x.com.samsung.da.modes')
|
||||
if isinstance(modes, (list, tuple)):
|
||||
return modes[0] if modes else None
|
||||
return modes
|
||||
|
||||
|
||||
MODE = Capability(
|
||||
href='/mode/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SelectDesc(key='operating_mode', rep_fn=_first_mode,
|
||||
icon='mdi:tune-variant',
|
||||
options_field='x.com.samsung.da.supportedModes',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['mode', 'vs', '0'], {'x.com.samsung.da.modes': [p]})),
|
||||
),
|
||||
)
|
||||
|
||||
# Target humidity is this device's primary control (the issue-#88 dump's
|
||||
# equivalent of a thermostat setpoint). No min/max range field is present in
|
||||
# any dump seen so far -- native_min/native_max are deliberately left unset
|
||||
# so the number entity falls back to HA's own 0-100 default, the natural
|
||||
# bound for a percentage field, rather than a bound guessed from one unit's
|
||||
# spec sheet (see the adding-device-support skill's "never hard-code the one
|
||||
# dump's values" section). Step comes live from the device's own `increment`
|
||||
# field.
|
||||
HUMIDITY = Capability(
|
||||
href='/humidity/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SensorDesc(key='humidity', field='x.com.samsung.da.humidity',
|
||||
device_class='humidity', unit='%', state_class='measurement',
|
||||
value_fn=int_or_none),
|
||||
NumberDesc(key='target_humidity', field='x.com.samsung.da.desiredHumidity',
|
||||
device_class='humidity', unit='%',
|
||||
icon='mdi:water-percent',
|
||||
entity_category='config',
|
||||
value_fn=int_or_none,
|
||||
step_fn=lambda rep: int_or_none(rep.get('increment')) or 1,
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['humidity', 'vs', '0'],
|
||||
{'x.com.samsung.da.desiredHumidity': str(int(round(float(p))))})),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dehumidifier-scoped coverage: vendor plumbing with no user-actionable state
|
||||
# or no documented write contract, following the same 'don't guess' rule as
|
||||
# airconditioner._AC_IGNORED (this is the same DA_AC_ board family). Not in
|
||||
# the global ignored.IGNORED since some of these hrefs collide with other
|
||||
# families' schemas.
|
||||
# ---------------------------------------------------------------------------
|
||||
_DHM_IGNORED = [
|
||||
'/availablecontrolsets/vs/0', # opaque hex-encoded control-set bitmap (id: DHM)
|
||||
'/da/softreset/vs/0', # soft-reset trigger plumbing
|
||||
'/keepnormalstate/vs/0', # internal keep-normal flag
|
||||
'/personality/presence/vs/0', # presence-personalization plumbing (empty item value)
|
||||
'/reserverulesets/vs/0', # opaque hex-encoded schedule reservation blob
|
||||
'/sensors/vs/0', # empty {} on this dump
|
||||
'/welcome/humidity/vs/0', # welcome-mode plumbing (requestId/operatingStatus, inert)
|
||||
# Only supportedModes ([Off, Sleep]) is present -- no live "current
|
||||
# value" field on this dump to confirm the read/write contract, so per
|
||||
# the 'don't guess' rule this is left unmodeled rather than assumed.
|
||||
'/mode/convenient/vs/0',
|
||||
]
|
||||
|
||||
COVERAGE = [Capability(href=h) for h in _DHM_IGNORED]
|
||||
@@ -42,7 +42,10 @@ DRYER_SETTINGS = Capability(
|
||||
# under entity.select.dryer_cycle (Table_03, DV5000-class, captured
|
||||
# 2026-05-29). Codes 0x21 and 0x4C appear in the issue #14 DV90BB5245AES1
|
||||
# editCourseList but aren't identified yet -- they render as the raw code
|
||||
# until named. The /st/dryercourse/vs/0 resource re-encodes the same selected
|
||||
# until named. Codes '01' Normal and '06' Time dry were confirmed on a
|
||||
# DVE50A8600V/A3 (also Table_03) by selecting each cycle on the physical
|
||||
# appliance and reading back the raw code from the entity's state (issue
|
||||
# #80). The /st/dryercourse/vs/0 resource re-encodes the same selected
|
||||
# course and is ignored (ignored.py) -- the mirror of how /st/washercourse/vs/0
|
||||
# is ignored for washers.
|
||||
DRYER_COURSE = Capability(
|
||||
|
||||
@@ -62,7 +62,38 @@ TEMP_CURRENT_GENERIC = Capability(
|
||||
),
|
||||
)
|
||||
|
||||
TEMP_SETPOINT_GENERIC = Capability(
|
||||
def _temp_setpoint_write(p, rep, href=None, resources=None):
|
||||
"""Write temperature — prefer vendor /temperatures/vs/0 when available,
|
||||
fall back to direct OCF /temperature/desired/ write otherwise.
|
||||
|
||||
Samsung fridges expose both OCF-standard /temperature/desired/* and vendor
|
||||
/temperatures/vs/0. On some models only the vendor path commits the change;
|
||||
on others both work. Using the vendor path when present is always correct.
|
||||
Item IDs follow the Samsung convention: "0" = Freezer, "1" = Fridge/Cooler.
|
||||
"""
|
||||
if not href:
|
||||
return None
|
||||
if resources and '/temperatures/vs/0' in resources:
|
||||
if '/cooler/' in href:
|
||||
item_id = '1'
|
||||
elif '/freezer/' in href:
|
||||
item_id = '0'
|
||||
else:
|
||||
return None
|
||||
return (
|
||||
['temperatures', 'vs', '0'],
|
||||
{'x.com.samsung.da.items': [
|
||||
{'x.com.samsung.da.id': item_id,
|
||||
'x.com.samsung.da.desired': str(int(round(float(p))))}
|
||||
]}
|
||||
)
|
||||
return (
|
||||
[s for s in href.strip('/').split('/') if s],
|
||||
{'temperature': int(round(float(p)))}
|
||||
)
|
||||
|
||||
|
||||
TEMP_SETPOINT = Capability(
|
||||
href=None,
|
||||
href_prefix='/temperature/desired/',
|
||||
strip_prefix_in_key=True,
|
||||
@@ -73,10 +104,7 @@ TEMP_SETPOINT_GENERIC = Capability(
|
||||
use_instance_name=True, device_class='temperature', unit_fn=_temp_unit,
|
||||
native_min=-20.0, native_max=50.0,
|
||||
range_field='range', entity_category='config',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
[s for s in href.strip('/').split('/') if s],
|
||||
{'temperature': int(round(float(p)))}
|
||||
) if href else None),
|
||||
write_fn=_temp_setpoint_write),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -573,15 +601,27 @@ FLEX_ZONE = Capability(
|
||||
# Generic door pattern capability (href=None — use as pattern_cap only)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _door_open_state(rep):
|
||||
"""Most /door/* resources report bare `openState`, but the
|
||||
ARTIK051_DONGLE_REF family's /door/onedoorfreezer/vs/0 (issues #77, #83)
|
||||
reports the vendor-prefixed `x.com.samsung.da.openState` instead. This
|
||||
capability still binds either way (href_prefix match doesn't care about
|
||||
field names), but a plain `field=` lookup against the wrong key means
|
||||
the entity exists and is permanently unavailable -- check both."""
|
||||
v = rep.get('openState')
|
||||
if v is None:
|
||||
v = rep.get('x.com.samsung.da.openState')
|
||||
return v == 'Open'
|
||||
|
||||
|
||||
DOOR_GENERIC = Capability(
|
||||
href=None,
|
||||
href_prefix='/door/',
|
||||
poll_tier='hot',
|
||||
entities=(
|
||||
BinarySensorDesc(key='open', field='openState',
|
||||
BinarySensorDesc(key='open', rep_fn=_door_open_state,
|
||||
translation_key='instance_open',
|
||||
use_instance_name=True, device_class='door',
|
||||
value_fn=lambda v: v == 'Open'),
|
||||
use_instance_name=True, device_class='door'),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -131,4 +131,12 @@ IGNORED: list[Capability] = [
|
||||
# parallel write-capable capability around an unverified generic OCF
|
||||
# write contract.
|
||||
Capability(href='/operational/state/0'),
|
||||
|
||||
# Cooktop guided-cooking/recipe status (issue #86, TP1X_DA-KS-COOKTOP
|
||||
# family): sequenceNumber, operationBurnerNumber, a stageInfo block, and
|
||||
# a textData.menu string -- every field empty/zero on the only dump
|
||||
# seen so far (device idle, no guided-cooking program active). Same
|
||||
# "don't guess" treatment as the microwave family's /recipe/cook/vs/0.
|
||||
# Revisit if a dump with an active recipe surfaces.
|
||||
Capability(href='/cooktop/recipe/status/vs/0'),
|
||||
]
|
||||
|
||||
@@ -338,6 +338,12 @@ OVEN_CONNECTED = Capability(
|
||||
# fields worth exposing.
|
||||
OVEN_SPEC = Capability(href='/oven/spec/vs/0')
|
||||
|
||||
# Quick-recipe display blob (combi microwave, issue #121) -- a JSON-encoded
|
||||
# string (language/menu/servingSize/option) with every field blank on the
|
||||
# only dump seen, and no documented write contract. No entity to bind per
|
||||
# the 'don't guess' rule; a bare Capability still marks the href covered.
|
||||
OVEN_RECIPE_COOK = Capability(href='/recipe/cook/vs/0')
|
||||
|
||||
OVEN_MODE = Capability(
|
||||
href='/mode/vs/0',
|
||||
poll_tier='warm',
|
||||
|
||||
@@ -24,7 +24,8 @@ write pattern already proven safe elsewhere in this codebase (oven setpoint,
|
||||
icemaker toggles).
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc
|
||||
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc, SwitchDesc
|
||||
from .common import normalize_temp_unit
|
||||
|
||||
# Observed as high as 4 (this issue's dump); user-reported hardware with 5
|
||||
# burners exists. Kept a little above both since exists_fn gates unused
|
||||
@@ -52,6 +53,10 @@ def _burner_hot_surface_fn(i):
|
||||
return lambda burner_list: get_state(burner_list) not in (None, 'normal')
|
||||
|
||||
|
||||
def _burner_pan_detection_fn(i):
|
||||
return lambda burner_list: bool(_burner_field_fn(i, 'panDetection')(burner_list))
|
||||
|
||||
|
||||
def _power_level_options(resources):
|
||||
spec = resources.get('/cooktop/spec/vs/0') or {}
|
||||
return list(spec.get('supportedPowerLevelList') or [])
|
||||
@@ -100,15 +105,44 @@ def _burner_entities(i):
|
||||
translation_placeholders={'number': str(n)},
|
||||
exists_fn=exists,
|
||||
value_fn=_burner_hot_surface_fn(i)),
|
||||
BinarySensorDesc(key=f'burner_{i}_pan_detected', field='burnerList',
|
||||
icon='mdi:pot',
|
||||
translation_key='burner_pan_detected',
|
||||
translation_placeholders={'number': str(n)},
|
||||
entity_category='diagnostic',
|
||||
exists_fn=exists,
|
||||
value_fn=_burner_pan_detection_fn(i)),
|
||||
)
|
||||
|
||||
|
||||
def _child_lock_write(p, rep, href=None):
|
||||
if p not in ('On', 'Off'):
|
||||
return None
|
||||
return ['cooktop', 'status', 'vs', '0'], {'childLock': p.lower()}
|
||||
|
||||
|
||||
COOKTOP_STATUS = Capability(
|
||||
href='/cooktop/status/vs/0',
|
||||
poll_tier='hot',
|
||||
entities=(
|
||||
SensorDesc(key='cooktop_state', field='operationState',
|
||||
icon='mdi:pot-steam'),
|
||||
# The cooktop section's own on/off (issue #86) -- distinct from
|
||||
# common.POWER's /power/0 or /power/vs/0, which some boards in this
|
||||
# family (the range combo) additionally carry for the whole
|
||||
# appliance. Read-only: no live device to confirm remotely turning
|
||||
# a cooktop on wouldn't leave a burner active unattended.
|
||||
BinarySensorDesc(key='cooktop_power', field='power',
|
||||
device_class='power', icon='mdi:pot-steam',
|
||||
value_fn=lambda v: str(v).lower() == 'on'),
|
||||
# Safe to write -- a lock toggle, not a heat control -- via a
|
||||
# direct single-field PUT (no RMW needed; unlike burnerList this
|
||||
# is a lone scalar, not an array of siblings to preserve).
|
||||
SwitchDesc(key='cooktop_child_lock', field='childLock',
|
||||
device_class='lock', entity_category='config',
|
||||
icon='mdi:lock',
|
||||
value_fn=lambda v: str(v).lower() == 'on',
|
||||
write_fn=_child_lock_write),
|
||||
*[e for i in range(MAX_BURNERS) for e in _burner_entities(i)],
|
||||
),
|
||||
)
|
||||
@@ -133,3 +167,46 @@ COOKTOP_SAFETY = Capability(
|
||||
value_fn=lambda v: (v or {}).get('state') == 'on'),
|
||||
),
|
||||
)
|
||||
|
||||
# Bluetooth meat probe (issue #86). All-idle sentinel values when
|
||||
# disconnected (operationBurnerNumber -1, temperatures 0) -- no special
|
||||
# gating on those, matching cooktop.PAIRED_HOOD_STATUS's own precedent of
|
||||
# showing a disconnected accessory's fields plainly rather than hiding the
|
||||
# whole capability.
|
||||
PROBE_STATUS = Capability(
|
||||
href='/bluetooth/probe/status/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
BinarySensorDesc(key='probe_connected', field='connectionState',
|
||||
device_class='connectivity',
|
||||
value_fn=lambda v: str(v).lower() == 'connected'),
|
||||
SensorDesc(key='probe_battery', field='batteryPercentage',
|
||||
device_class='battery', state_class='measurement',
|
||||
unit='%', entity_category='diagnostic'),
|
||||
SensorDesc(key='probe_temperature', field='currentTemperature',
|
||||
device_class='temperature', state_class='measurement',
|
||||
unit_fn=lambda rep: normalize_temp_unit(rep.get('temperatureUnit'), '°C')),
|
||||
SensorDesc(key='probe_target_temperature', field='targetTemperature',
|
||||
device_class='temperature', entity_category='diagnostic',
|
||||
unit_fn=lambda rep: normalize_temp_unit(rep.get('temperatureUnit'), '°C')),
|
||||
),
|
||||
)
|
||||
|
||||
# Some range boards (issue #74's NE63B8411SS) report no /cooktop/status/vs/0
|
||||
# burner array at all -- their local API only exposes this coarse
|
||||
# monitoring resource for the cooktop half, with no per-burner detail.
|
||||
# Meaning of `cooktopMonitoring` (a bare "0" on the only dump seen) and
|
||||
# `warmingCenterState`'s full value set aren't confirmed, so both are
|
||||
# exposed as plain sensors rather than guessed at as a switch/select --
|
||||
# `supportedHoodLampStateList` has no corresponding live-state field on
|
||||
# this resource, so nothing to bind it to yet.
|
||||
COOKTOP_MONITORING = Capability(
|
||||
href='/cooktopmonitoring/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SensorDesc(key='cooktop_running_state', field='x.com.samsung.da.cooktopRunningState',
|
||||
icon='mdi:pot-steam-outline'),
|
||||
SensorDesc(key='warming_center_state', field='x.com.samsung.da.warmingCenterState',
|
||||
icon='mdi:heat-wave', entity_category='diagnostic'),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Capabilities for the Samsung stick-vacuum clean/auto-empty station
|
||||
(model A-VSKR-TP1-22-VS9500AL, "Bespoke Jet" clean station, issue #131).
|
||||
|
||||
The reporter's diagnostics dump shows no vacuum-body state at all (no
|
||||
suction level, no battery, no cleaning-mode/room-mapping control, no
|
||||
docked/undocked status even) -- only the clean station's own dustbag,
|
||||
dustbin auto-empty settings, and UV-C sanitizing-cycle status. This
|
||||
strongly suggests the WiFi/DTLS module lives in the station, not the
|
||||
handheld stick: the station is the only "device" this integration's local
|
||||
API can see, and the wand's own controls are unrelated hardware not
|
||||
reachable this way. Modeled as its own device type -- these hrefs don't
|
||||
overlap with any existing family, so there's no shared-href ambiguity to
|
||||
resolve against another type (see registry/by_type/__init__.py's
|
||||
docstring for that rule).
|
||||
|
||||
Resources verified against the issue #131 diagnostics dump.
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc, SwitchDesc
|
||||
from .common import int_or_none, parse_iso_utc as _parse_iso_utc
|
||||
|
||||
|
||||
DUSTBAG = Capability(
|
||||
href='/component/station/dustbag/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
BinarySensorDesc(key='dustbag_full', field='x.com.samsung.da.status',
|
||||
device_class='problem',
|
||||
icon='mdi:bag-personal',
|
||||
value_fn=lambda v: v == 'full'),
|
||||
),
|
||||
)
|
||||
|
||||
# dustbagUsage/dustbagPrevUsage are raw counters with no capacity/resolution
|
||||
# field alongside them to normalize into a percentage (unlike the AC/range
|
||||
# families' filterUsage, which always ships filterCapacity) -- exposed as a
|
||||
# plain diagnostic count rather than guessing a unit.
|
||||
DUSTBAG_USAGE = Capability(
|
||||
href='/component/station/dustbagusage/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SensorDesc(key='dustbag_usage', field='x.com.samsung.da.dustbagUsage',
|
||||
icon='mdi:counter', entity_category='diagnostic',
|
||||
state_class='total_increasing', value_fn=int_or_none),
|
||||
),
|
||||
)
|
||||
|
||||
DUSTBIN_SETTING = Capability(
|
||||
href='/setting/dustbin/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SwitchDesc(key='auto_empty', field='x.com.samsung.da.autoEmpty',
|
||||
icon='mdi:delete-empty',
|
||||
entity_category='config',
|
||||
value_fn=lambda v: v == 'On',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['setting', 'dustbin', 'vs', '0'],
|
||||
{'x.com.samsung.da.autoEmpty': 'On' if p == 'On' else 'Off'})),
|
||||
SwitchDesc(key='dustbin_auto_close', field='x.com.samsung.da.autoClose',
|
||||
icon='mdi:door-sliding',
|
||||
entity_category='config',
|
||||
value_fn=lambda v: v == 'On',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['setting', 'dustbin', 'vs', '0'],
|
||||
{'x.com.samsung.da.autoClose': 'On' if p == 'On' else 'Off'})),
|
||||
SelectDesc(key='discharging_time', field='x.com.samsung.da.desiredDischargingTime',
|
||||
icon='mdi:timer-outline',
|
||||
entity_category='config',
|
||||
options_field='x.com.samsung.da.supportedDischargingTime',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['setting', 'dustbin', 'vs', '0'],
|
||||
{'x.com.samsung.da.desiredDischargingTime': p})),
|
||||
),
|
||||
)
|
||||
|
||||
# stickStatus's exact meaning (docked? powered? charging?) isn't confirmed
|
||||
# from this single On/Off dump -- exposed as a plain diagnostic sensor
|
||||
# rather than a binary_sensor, so no polarity/semantic is asserted that
|
||||
# might be wrong.
|
||||
CLEANSTATION_STATUS = Capability(
|
||||
href='/status/cleanstation/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SensorDesc(key='cleanstation_status', field='x.com.samsung.da.status',
|
||||
icon='mdi:home-lightning-bolt', entity_category='diagnostic'),
|
||||
SensorDesc(key='stick_status', field='x.com.samsung.da.stickStatus',
|
||||
icon='mdi:broom', entity_category='diagnostic'),
|
||||
SwitchDesc(key='uvc_intensive_mode', field='x.com.samsung.da.uvcIntensive',
|
||||
icon='mdi:lightbulb-on-outline',
|
||||
entity_category='config',
|
||||
value_fn=lambda v: v == 'On',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['status', 'cleanstation', 'vs', '0'],
|
||||
{'x.com.samsung.da.uvcIntensive': 'On' if p == 'On' else 'Off'})),
|
||||
SensorDesc(key='uvc_operation_time', field='x.com.samsung.da.uvcOperationTime',
|
||||
icon='mdi:timer-sand', entity_category='diagnostic',
|
||||
value_fn=int_or_none),
|
||||
SensorDesc(key='uvc_total_operation_time',
|
||||
field='x.com.samsung.da.uvcTotalOperationTime',
|
||||
icon='mdi:timer-sand', entity_category='diagnostic',
|
||||
state_class='total_increasing', value_fn=int_or_none),
|
||||
SensorDesc(key='uvc_finished_time', field='x.com.samsung.da.uvcFinishedTime',
|
||||
device_class='timestamp', entity_category='diagnostic',
|
||||
value_fn=_parse_iso_utc),
|
||||
SensorDesc(key='uvc_emitted_time', field='x.com.samsung.da.emittedTime',
|
||||
device_class='timestamp', entity_category='diagnostic',
|
||||
value_fn=_parse_iso_utc),
|
||||
),
|
||||
)
|
||||
@@ -45,6 +45,14 @@ from .laundry import (
|
||||
# (issue #22). Combo units carry their own course set, so these codes
|
||||
# don't imply anything about '1F' on a plain washer.
|
||||
#
|
||||
# Three more -- '52' Eco Cold, '54' Towels, '60' Self Clean+ -- came from a
|
||||
# WF50A8600AV/US, verified directly rather than by inference: the reporter
|
||||
# selected each cycle on the physical appliance and read back the resulting
|
||||
# raw code from the cycle_select entity's state (issue #80). '54' shares a
|
||||
# display name with the existing '24' Towels -- a different code on a
|
||||
# different course table legitimately landing on the same label, not a typo
|
||||
# (same pattern as '21'/'65' Colors and '27'/'5E' Rinse+Spin above).
|
||||
#
|
||||
# No static fallback list of those codes is kept here, deliberately: other
|
||||
# washer models have a different actual course set (a second dump's active
|
||||
# course, '65', isn't even in the list above; models with 'AI Wash'/'Mixed
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
"""Capabilities for the Samsung water-purifier family (TP2X_WATERPURIFIER-class,
|
||||
issue #90, model TP2X_WATERPURIFIER_20K).
|
||||
|
||||
Resources verified against the issue #90 diagnostics dump.
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import BinarySensorDesc, NumberDesc, SelectDesc, SensorDesc, SwitchDesc
|
||||
from .common import int_or_none, parse_iso_utc as _parse_iso_utc
|
||||
|
||||
|
||||
DISPENSE = Capability(
|
||||
href='/setting/waterpurifier/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SelectDesc(key='dispense_type', field='x.com.samsung.da.desiredType',
|
||||
icon='mdi:cup-water',
|
||||
options_field='x.com.samsung.da.supportedTypes',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['setting', 'waterpurifier', 'vs', '0'],
|
||||
{'x.com.samsung.da.desiredType': p})),
|
||||
# Only a handful of discrete temperatures are selectable (not a
|
||||
# continuous range) -- a select over the live-reported set, not a
|
||||
# number with invented bounds.
|
||||
SelectDesc(key='hot_water_temperature', field='x.com.samsung.da.tempDesiredHotWater',
|
||||
icon='mdi:thermometer',
|
||||
entity_category='config',
|
||||
options_field='x.com.samsung.da.supportedHotTemperatures',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['setting', 'waterpurifier', 'vs', '0'],
|
||||
{'x.com.samsung.da.tempDesiredHotWater': p})),
|
||||
# Bounds and step come live from the device's own
|
||||
# desiredCapacityRange/capacityResolution fields, not a hardcoded
|
||||
# constant -- see the adding-device-support skill's "never hard-code
|
||||
# the one dump's values" section. No unit is set: capacityUnit reads
|
||||
# "C" on this dump, which can't be right for a volume field, so per
|
||||
# the 'don't guess' rule the unit is left unset rather than assumed
|
||||
# to be mL.
|
||||
NumberDesc(key='dispense_capacity', field='x.com.samsung.da.desiredCapacity',
|
||||
icon='mdi:cup-water',
|
||||
value_fn=int_or_none,
|
||||
range_field='x.com.samsung.da.desiredCapacityRange',
|
||||
step_fn=lambda rep: int_or_none(
|
||||
rep.get('x.com.samsung.da.capacityResolution')) or 1,
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['setting', 'waterpurifier', 'vs', '0'],
|
||||
{'x.com.samsung.da.desiredCapacity': str(int(round(float(p))))})),
|
||||
BinarySensorDesc(key='pouring', field='x.com.samsung.da.pourStatus',
|
||||
icon='mdi:cup-water',
|
||||
value_fn=lambda v: v == 'On'),
|
||||
),
|
||||
)
|
||||
|
||||
STATUS = Capability(
|
||||
href='/status/waterpurifier/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SensorDesc(key='waterpurifier_status', field='x.com.samsung.da.status',
|
||||
icon='mdi:water-pump',
|
||||
entity_category='diagnostic'),
|
||||
BinarySensorDesc(key='filter_door_status', field='x.com.samsung.da.filterDoorStatus',
|
||||
device_class='door',
|
||||
entity_category='diagnostic',
|
||||
value_fn=lambda v: v == 'Open'),
|
||||
SensorDesc(key='sterilize_period', field='x.com.samsung.da.sterilizePeriod',
|
||||
icon='mdi:calendar-sync',
|
||||
entity_category='diagnostic'),
|
||||
SensorDesc(key='sterilize_run_time', field='x.com.samsung.da.sterilizeRunTime',
|
||||
icon='mdi:timer-outline',
|
||||
entity_category='diagnostic'),
|
||||
SensorDesc(key='sterilize_last_time', device_class='timestamp',
|
||||
entity_category='diagnostic',
|
||||
rep_fn=lambda rep: _parse_iso_utc(
|
||||
rep.get('x.com.samsung.da.sterilizeLastTime'))),
|
||||
SensorDesc(key='sterilize_plan_time', device_class='timestamp',
|
||||
entity_category='diagnostic',
|
||||
rep_fn=lambda rep: _parse_iso_utc(
|
||||
rep.get('x.com.samsung.da.sterilizePlanTime'))),
|
||||
SensorDesc(key='filter_clean_remain_time', field='x.com.samsung.da.filterCleanRemainTime',
|
||||
icon='mdi:timer-sand',
|
||||
entity_category='diagnostic'),
|
||||
),
|
||||
)
|
||||
|
||||
FAVORITE_CAPACITY = Capability(
|
||||
href='/favorite/capacity/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SwitchDesc(key='favorite_capacity_enabled', field='x.com.samsung.da.switchCapacity',
|
||||
icon='mdi:star-outline',
|
||||
entity_category='config',
|
||||
value_fn=lambda v: v == 'On',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['favorite', 'capacity', 'vs', '0'],
|
||||
{'x.com.samsung.da.switchCapacity': 'On' if p == 'On' else 'Off'})),
|
||||
SelectDesc(key='favorite_capacity', field='x.com.samsung.da.defaultCapacity',
|
||||
icon='mdi:cup-water',
|
||||
entity_category='config',
|
||||
options_field='x.com.samsung.da.capacityList',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['favorite', 'capacity', 'vs', '0'],
|
||||
{'x.com.samsung.da.defaultCapacity': p})),
|
||||
),
|
||||
)
|
||||
|
||||
# Coffee-capable variant (issue #107) -- same "favorite" enable-toggle +
|
||||
# supported-list select shape as FAVORITE_CAPACITY above, but for the hot
|
||||
# water dispensed alongside brewing rather than the pour capacity.
|
||||
FAVORITE_HOTWATER = Capability(
|
||||
href='/favorite/hotwater/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SwitchDesc(key='favorite_hotwater_enabled', field='x.com.samsung.da.switchHotwater',
|
||||
icon='mdi:star-outline',
|
||||
entity_category='config',
|
||||
value_fn=lambda v: v != 'Locked',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['favorite', 'hotwater', 'vs', '0'],
|
||||
{'x.com.samsung.da.switchHotwater': 'Unlocked' if p == 'On' else 'Locked'})),
|
||||
SelectDesc(key='favorite_hotwater_temperature',
|
||||
field='x.com.samsung.da.favorite.defaultTemperature',
|
||||
icon='mdi:thermometer',
|
||||
entity_category='config',
|
||||
options_field='x.com.samsung.da.favorite.supportedList',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['favorite', 'hotwater', 'vs', '0'],
|
||||
{'x.com.samsung.da.favorite.defaultTemperature': p})),
|
||||
),
|
||||
)
|
||||
|
||||
# Coffee-capable variant (issue #107). No 'x.com.samsung.da.' field prefix
|
||||
# on this resource, unlike the rest of the water-purifier surface.
|
||||
COFFEE = Capability(
|
||||
href='/favorite/coffee/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SwitchDesc(key='favorite_coffee_enabled', field='favorite.activate',
|
||||
icon='mdi:coffee-outline',
|
||||
entity_category='config',
|
||||
value_fn=lambda v: v == 'On',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['favorite', 'coffee', 'vs', '0'],
|
||||
{'favorite.activate': 'On' if p == 'On' else 'Off'})),
|
||||
SensorDesc(key='coffee_brew_status', field='brew.status',
|
||||
icon='mdi:coffee-outline',
|
||||
entity_category='diagnostic'),
|
||||
),
|
||||
)
|
||||
|
||||
LOCK = Capability(
|
||||
href='/status/lock/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
SwitchDesc(key='hotwater_lock', field='x.com.samsung.da.hotwaterLock',
|
||||
device_class='lock',
|
||||
entity_category='config',
|
||||
value_fn=lambda v: v != 'Unlocked',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['status', 'lock', 'vs', '0'],
|
||||
{'x.com.samsung.da.hotwaterLock': 'Locked' if p == 'On' else 'Unlocked'})),
|
||||
SwitchDesc(key='coldwater_lock', field='x.com.samsung.da.coldwaterLock',
|
||||
device_class='lock',
|
||||
entity_category='config',
|
||||
value_fn=lambda v: v != 'Unlocked',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['status', 'lock', 'vs', '0'],
|
||||
{'x.com.samsung.da.coldwaterLock': 'Locked' if p == 'On' else 'Unlocked'})),
|
||||
SwitchDesc(key='buzz_lock', field='x.com.samsung.da.buzzLock',
|
||||
device_class='lock',
|
||||
entity_category='config',
|
||||
value_fn=lambda v: v != 'Unlocked',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['status', 'lock', 'vs', '0'],
|
||||
{'x.com.samsung.da.buzzLock': 'Locked' if p == 'On' else 'Unlocked'})),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Water-purifier-scoped coverage: hrefs with no user-actionable state or no
|
||||
# confirmed contract, following the 'don't guess' rule.
|
||||
# ---------------------------------------------------------------------------
|
||||
_WP_IGNORED = [
|
||||
# supportedModes carries a single opaque wizard-workflow token
|
||||
# ('HOMECARE_WIZARD_V2') and modes reports a completely different,
|
||||
# unrelated value ('WATERFILTER_DISABLE') not even present in
|
||||
# supportedModes -- internal plumbing, not a real user-facing mode
|
||||
# select. OCF-standard /mode/0 mirrors the same vendor resource but is
|
||||
# already covered by the global ignored.IGNORED (fridge's OCF-native
|
||||
# vacation-mode flag shares that href).
|
||||
'/mode/vs/0',
|
||||
# Static support-flags blob (automation.supported.modes/options) -- no
|
||||
# live "current automation setting" field to expose.
|
||||
'/automation/waterpurifier/vs/0',
|
||||
# Coffee-capable variant (issue #107). All four are static
|
||||
# capability-advertisement blobs or empty -- no live "current recipe" /
|
||||
# "current custom slot" field to expose, unlike /favorite/coffee/vs/0
|
||||
# (COFFEE above), which does carry live brew status.
|
||||
'/brand/recipe/info/vs/0', # revision + max-brand-count metadata
|
||||
'/coffee/custom/recipe/vs/0', # publisher.support: allowed custom-recipe slot IDs
|
||||
'/recipe/coffee/vs/0', # same publisher.support shape, no per-recipe content
|
||||
'/recipe/coffee/deletion/vs/0', # empty {} on this dump
|
||||
]
|
||||
|
||||
COVERAGE = [Capability(href=h) for h in _WP_IGNORED]
|
||||
@@ -10,15 +10,27 @@
|
||||
"burner_hot_surface": {
|
||||
"name": "Burner {number} hot surface"
|
||||
},
|
||||
"burner_pan_detected": {
|
||||
"name": "Burner {number} pan detected"
|
||||
},
|
||||
"cloud_connected": {
|
||||
"name": "Cloud connected"
|
||||
},
|
||||
"dustbag_full": {
|
||||
"name": "Dust bag full"
|
||||
},
|
||||
"cooktop_power": {
|
||||
"name": "Cooktop power"
|
||||
},
|
||||
"cooktop_safety_shutoff_enabled": {
|
||||
"name": "Hot surface auto-shutoff enabled"
|
||||
},
|
||||
"current_limit_enabled": {
|
||||
"name": "Current limit enabled"
|
||||
},
|
||||
"overload_protection_active": {
|
||||
"name": "Overload protection active"
|
||||
},
|
||||
"cycle_active": {
|
||||
"name": "Running"
|
||||
},
|
||||
@@ -34,6 +46,9 @@
|
||||
"door_open": {
|
||||
"name": "Door"
|
||||
},
|
||||
"filter_door_status": {
|
||||
"name": "Filter door"
|
||||
},
|
||||
"firmware_update": {
|
||||
"name": "Firmware update available"
|
||||
},
|
||||
@@ -52,9 +67,18 @@
|
||||
"periodic_air_sensing": {
|
||||
"name": "Periodic air sensing"
|
||||
},
|
||||
"pouring": {
|
||||
"name": "Pouring"
|
||||
},
|
||||
"power_state": {
|
||||
"name": "Power state"
|
||||
},
|
||||
"probe_connected": {
|
||||
"name": "Probe connected"
|
||||
},
|
||||
"power_switch": {
|
||||
"name": "Power"
|
||||
},
|
||||
"rapid_freezing": {
|
||||
"name": "Rapid freezing"
|
||||
},
|
||||
@@ -95,9 +119,31 @@
|
||||
},
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"ai_comfort": "AI Comfort",
|
||||
"quiet": "Quiet",
|
||||
"smart": "Smart",
|
||||
"speed": "Speed"
|
||||
"speed": "Speed",
|
||||
"nano": "WindFree",
|
||||
"nanosleep": "WindFree sleep",
|
||||
"longwind": "Long wind",
|
||||
"motionindirect": "Motion indirect",
|
||||
"motiondirect": "Motion direct",
|
||||
"drycomfort": "Dry comfort"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"air_purifier_fan": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"smart": "Smart",
|
||||
"max": "Max",
|
||||
"mid": "Mid",
|
||||
"windfree": "WindFree",
|
||||
"sleep": "Sleep"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,6 +156,9 @@
|
||||
"delay_start_hours": {
|
||||
"name": "Delay start"
|
||||
},
|
||||
"dispense_capacity": {
|
||||
"name": "Dispense capacity"
|
||||
},
|
||||
"instance_setpoint": {
|
||||
"name": "{instance_name} setpoint"
|
||||
},
|
||||
@@ -118,6 +167,9 @@
|
||||
},
|
||||
"sound_volume": {
|
||||
"name": "Sound volume"
|
||||
},
|
||||
"target_humidity": {
|
||||
"name": "Target humidity"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
@@ -154,6 +206,13 @@
|
||||
"on": "On"
|
||||
}
|
||||
},
|
||||
"discharging_time": {
|
||||
"name": "Discharging time",
|
||||
"state": {
|
||||
"1": "1 minute",
|
||||
"3": "3 minutes"
|
||||
}
|
||||
},
|
||||
"cycle": {
|
||||
"name": "Cycle"
|
||||
},
|
||||
@@ -189,6 +248,9 @@
|
||||
"8f": "Baby Care"
|
||||
}
|
||||
},
|
||||
"dispense_type": {
|
||||
"name": "Dispense type"
|
||||
},
|
||||
"door_alert": {
|
||||
"name": "Door alarm",
|
||||
"state": {
|
||||
@@ -201,6 +263,8 @@
|
||||
"dryer_cycle_table_03": {
|
||||
"name": "Cycle",
|
||||
"state": {
|
||||
"01": "Normal",
|
||||
"06": "Time dry",
|
||||
"16": "Cotton",
|
||||
"18": "Synthetics",
|
||||
"19": "Delicates",
|
||||
@@ -217,6 +281,12 @@
|
||||
"1f": "Mixed load"
|
||||
}
|
||||
},
|
||||
"favorite_capacity": {
|
||||
"name": "Favorite capacity"
|
||||
},
|
||||
"favorite_hotwater_temperature": {
|
||||
"name": "Favorite hot water temperature"
|
||||
},
|
||||
"finish_sound": {
|
||||
"name": "Finish sound",
|
||||
"state": {
|
||||
@@ -248,6 +318,9 @@
|
||||
"extra_high": "Extra high"
|
||||
}
|
||||
},
|
||||
"hot_water_temperature": {
|
||||
"name": "Hot water temperature"
|
||||
},
|
||||
"ice_type": {
|
||||
"name": "{instance_name} type",
|
||||
"state": {
|
||||
@@ -286,6 +359,9 @@
|
||||
"air_fry": "Air fry"
|
||||
}
|
||||
},
|
||||
"operating_mode": {
|
||||
"name": "Operating mode"
|
||||
},
|
||||
"pantry_zone_mode": {
|
||||
"name": "Pantry zone mode",
|
||||
"state": {
|
||||
@@ -337,6 +413,13 @@
|
||||
"mute": "Mute"
|
||||
}
|
||||
},
|
||||
"air_purifier_sound_mode": {
|
||||
"name": "Sound mode",
|
||||
"state": {
|
||||
"mute": "Mute",
|
||||
"buzzer": "Buzzer"
|
||||
}
|
||||
},
|
||||
"spin_speed": {
|
||||
"name": "Spin speed",
|
||||
"state": {
|
||||
@@ -379,30 +462,37 @@
|
||||
"27": "Rinse+Spin",
|
||||
"28": "Drain/Spin",
|
||||
"29": "Drum Clean+",
|
||||
"2a": "Jeans",
|
||||
"2b": "AI Wash",
|
||||
"2d": "Silent Wash",
|
||||
"2e": "Baby Care",
|
||||
"2f": "Activewear",
|
||||
"30": "Cloudy Day",
|
||||
"32": "Shirts",
|
||||
"33": "Bedding",
|
||||
"34": "Mixed",
|
||||
"36": "Wash+Dry",
|
||||
"37": "Air Wash",
|
||||
"38": "Cotton Dry",
|
||||
"39": "Synthetics Dry",
|
||||
"3a": "Drum Clean",
|
||||
"52": "Eco Cold",
|
||||
"53": "Heavy Duty",
|
||||
"54": "Towels",
|
||||
"55": "Activewear",
|
||||
"57": "Delicate",
|
||||
"5e": "Rinse+Spin",
|
||||
"60": "Self Clean+",
|
||||
"65": "Colors",
|
||||
"66": "Denim",
|
||||
"7c": "Whites",
|
||||
"7d": "Bedding/Waterproof",
|
||||
"7e": "Self-Clean",
|
||||
"7f": "Wool/Delicate",
|
||||
"86": "Deep Wash",
|
||||
"87": "Download",
|
||||
"8f": "Intense Cold",
|
||||
"96": "Less Microfiber",
|
||||
"2b": "AI Wash",
|
||||
"2a": "Jeans"
|
||||
"96": "Less Microfiber"
|
||||
}
|
||||
},
|
||||
"washer_dry_level": {
|
||||
@@ -441,6 +531,9 @@
|
||||
"clean_level": {
|
||||
"name": "Clean level"
|
||||
},
|
||||
"coffee_brew_status": {
|
||||
"name": "Coffee brew status"
|
||||
},
|
||||
"connection_mode": {
|
||||
"name": "Connection mode",
|
||||
"state": {
|
||||
@@ -448,15 +541,58 @@
|
||||
"poll": "Polling"
|
||||
}
|
||||
},
|
||||
"cooktop_running_state": {
|
||||
"name": "Cooktop running state"
|
||||
},
|
||||
"cooktop_state": {
|
||||
"name": "Cooktop state"
|
||||
},
|
||||
"current_limit_level": {
|
||||
"name": "Current limit level"
|
||||
},
|
||||
"hepa_filter_usage": {
|
||||
"name": "HEPA filter usage"
|
||||
},
|
||||
"panel_status": {
|
||||
"name": "Panel status"
|
||||
},
|
||||
"sound_output": {
|
||||
"name": "Sound output"
|
||||
},
|
||||
"dustbag_usage": {
|
||||
"name": "Dust bag usage"
|
||||
},
|
||||
"cleanstation_status": {
|
||||
"name": "Clean station status"
|
||||
},
|
||||
"stick_status": {
|
||||
"name": "Stick status"
|
||||
},
|
||||
"uvc_operation_time": {
|
||||
"name": "UV-C operation time"
|
||||
},
|
||||
"uvc_total_operation_time": {
|
||||
"name": "UV-C total operation time"
|
||||
},
|
||||
"uvc_finished_time": {
|
||||
"name": "UV-C finished time"
|
||||
},
|
||||
"uvc_emitted_time": {
|
||||
"name": "UV-C emitted time"
|
||||
},
|
||||
"overload_protection_mode": {
|
||||
"name": "Overload protection mode",
|
||||
"state": {
|
||||
"alarm": "Alarm",
|
||||
"powersaving": "Power saving"
|
||||
}
|
||||
},
|
||||
"current_temp_c": {
|
||||
"name": "Temperature"
|
||||
},
|
||||
"current_temperature_c": {
|
||||
"name": "Temperature"
|
||||
},
|
||||
"diagnosis": {
|
||||
"name": "Diagnosis"
|
||||
},
|
||||
@@ -499,6 +635,9 @@
|
||||
"fan_speed_level": {
|
||||
"name": "Fan speed level"
|
||||
},
|
||||
"filter_clean_remain_time": {
|
||||
"name": "Filter clean remaining time"
|
||||
},
|
||||
"filter_progress": {
|
||||
"name": "Filter progress"
|
||||
},
|
||||
@@ -523,9 +662,15 @@
|
||||
"hood_filter_capacity": {
|
||||
"name": "Filter capacity"
|
||||
},
|
||||
"humidity": {
|
||||
"name": "Humidity"
|
||||
},
|
||||
"hood_filter_usage": {
|
||||
"name": "Filter usage"
|
||||
},
|
||||
"humidity": {
|
||||
"name": "Humidity"
|
||||
},
|
||||
"instance_temperature": {
|
||||
"name": "{instance_name} temperature"
|
||||
},
|
||||
@@ -574,6 +719,15 @@
|
||||
"power_watts": {
|
||||
"name": "Power"
|
||||
},
|
||||
"probe_battery": {
|
||||
"name": "Probe battery"
|
||||
},
|
||||
"probe_target_temperature": {
|
||||
"name": "Probe target temperature"
|
||||
},
|
||||
"probe_temperature": {
|
||||
"name": "Probe temperature"
|
||||
},
|
||||
"progress": {
|
||||
"name": "Progress"
|
||||
},
|
||||
@@ -589,12 +743,30 @@
|
||||
"selfcheck_status": {
|
||||
"name": "Self-check status"
|
||||
},
|
||||
"sterilize_last_time": {
|
||||
"name": "Sterilize last time"
|
||||
},
|
||||
"sterilize_period": {
|
||||
"name": "Sterilize period"
|
||||
},
|
||||
"sterilize_plan_time": {
|
||||
"name": "Sterilize plan time"
|
||||
},
|
||||
"sterilize_run_time": {
|
||||
"name": "Sterilize run time"
|
||||
},
|
||||
"super_fine_dust": {
|
||||
"name": "Super fine dust"
|
||||
},
|
||||
"warming_center_state": {
|
||||
"name": "Warming center state"
|
||||
},
|
||||
"water_liters": {
|
||||
"name": "Water consumption"
|
||||
},
|
||||
"waterpurifier_status": {
|
||||
"name": "Status"
|
||||
},
|
||||
"machine_state": {
|
||||
"name": "Machine state",
|
||||
"state": {
|
||||
@@ -629,6 +801,21 @@
|
||||
"auto_clean": {
|
||||
"name": "Auto clean"
|
||||
},
|
||||
"display": {
|
||||
"name": "Display"
|
||||
},
|
||||
"pet_filter_activation": {
|
||||
"name": "Pet filter activation"
|
||||
},
|
||||
"auto_empty": {
|
||||
"name": "Auto empty"
|
||||
},
|
||||
"dustbin_auto_close": {
|
||||
"name": "Dustbin auto-close"
|
||||
},
|
||||
"uvc_intensive_mode": {
|
||||
"name": "UV-C intensive mode"
|
||||
},
|
||||
"auto_door_opener": {
|
||||
"name": "Auto door opener"
|
||||
},
|
||||
@@ -641,6 +828,9 @@
|
||||
"bubble_soak": {
|
||||
"name": "Bubble soak"
|
||||
},
|
||||
"buzz_lock": {
|
||||
"name": "Buzzer lock"
|
||||
},
|
||||
"cabinet_light_dim": {
|
||||
"name": "Brighten gradually"
|
||||
},
|
||||
@@ -650,6 +840,12 @@
|
||||
"child_lock": {
|
||||
"name": "Child lock"
|
||||
},
|
||||
"coldwater_lock": {
|
||||
"name": "Cold water lock"
|
||||
},
|
||||
"cooktop_child_lock": {
|
||||
"name": "Child lock"
|
||||
},
|
||||
"defrost_delay": {
|
||||
"name": "Defrost delay"
|
||||
},
|
||||
@@ -659,9 +855,21 @@
|
||||
"fast_preheat": {
|
||||
"name": "Fast preheat"
|
||||
},
|
||||
"favorite_capacity_enabled": {
|
||||
"name": "Favorite capacity"
|
||||
},
|
||||
"favorite_coffee_enabled": {
|
||||
"name": "Favorite coffee"
|
||||
},
|
||||
"favorite_hotwater_enabled": {
|
||||
"name": "Favorite hot water"
|
||||
},
|
||||
"fridge_sound": {
|
||||
"name": "Sound"
|
||||
},
|
||||
"hotwater_lock": {
|
||||
"name": "Hot water lock"
|
||||
},
|
||||
"ice_maker_enabled": {
|
||||
"name": "Ice maker"
|
||||
},
|
||||
|
||||
@@ -10,15 +10,27 @@
|
||||
"burner_hot_surface": {
|
||||
"name": "Brander {number} heet oppervlak"
|
||||
},
|
||||
"burner_pan_detected": {
|
||||
"name": "Brander {number} pan gedetecteerd"
|
||||
},
|
||||
"cloud_connected": {
|
||||
"name": "Verbonden met cloud"
|
||||
},
|
||||
"dustbag_full": {
|
||||
"name": "Stofzak vol"
|
||||
},
|
||||
"cooktop_power": {
|
||||
"name": "Kookplaat voeding"
|
||||
},
|
||||
"cooktop_safety_shutoff_enabled": {
|
||||
"name": "Automatische uitschakeling heet oppervlak ingeschakeld"
|
||||
},
|
||||
"current_limit_enabled": {
|
||||
"name": "Stroombegrenzing ingeschakeld"
|
||||
},
|
||||
"overload_protection_active": {
|
||||
"name": "Overbelastingsbeveiliging actief"
|
||||
},
|
||||
"cycle_active": {
|
||||
"name": "Actief"
|
||||
},
|
||||
@@ -34,6 +46,9 @@
|
||||
"door_open": {
|
||||
"name": "Deur"
|
||||
},
|
||||
"filter_door_status": {
|
||||
"name": "Filterdeur"
|
||||
},
|
||||
"firmware_update": {
|
||||
"name": "Firmware-update beschikbaar"
|
||||
},
|
||||
@@ -52,9 +67,18 @@
|
||||
"periodic_air_sensing": {
|
||||
"name": "Periodieke luchtmeting"
|
||||
},
|
||||
"pouring": {
|
||||
"name": "Schenken"
|
||||
},
|
||||
"power_state": {
|
||||
"name": "Voedingsstatus"
|
||||
},
|
||||
"probe_connected": {
|
||||
"name": "Sonde verbonden"
|
||||
},
|
||||
"power_switch": {
|
||||
"name": "Voeding"
|
||||
},
|
||||
"rapid_freezing": {
|
||||
"name": "Snelvriezen"
|
||||
},
|
||||
@@ -95,9 +119,31 @@
|
||||
},
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"ai_comfort": "AI-comfort",
|
||||
"quiet": "Stil",
|
||||
"smart": "Slim",
|
||||
"speed": "Snel"
|
||||
"speed": "Snel",
|
||||
"nano": "WindFree",
|
||||
"nanosleep": "WindFree-slaap",
|
||||
"longwind": "Lange wind",
|
||||
"motionindirect": "Beweging indirect",
|
||||
"motiondirect": "Beweging direct",
|
||||
"drycomfort": "Droog comfort"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"air_purifier_fan": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"smart": "Slim",
|
||||
"max": "Max",
|
||||
"mid": "Gemiddeld",
|
||||
"windfree": "WindFree",
|
||||
"sleep": "Slaap"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,6 +156,9 @@
|
||||
"delay_start_hours": {
|
||||
"name": "Uitgestelde start"
|
||||
},
|
||||
"dispense_capacity": {
|
||||
"name": "Schenkhoeveelheid"
|
||||
},
|
||||
"instance_setpoint": {
|
||||
"name": "Instelpunt {instance_name}"
|
||||
},
|
||||
@@ -118,6 +167,9 @@
|
||||
},
|
||||
"sound_volume": {
|
||||
"name": "Geluidsvolume"
|
||||
},
|
||||
"target_humidity": {
|
||||
"name": "Doelvochtigheid"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
@@ -154,6 +206,13 @@
|
||||
"on": "Aan"
|
||||
}
|
||||
},
|
||||
"discharging_time": {
|
||||
"name": "Leegtijd",
|
||||
"state": {
|
||||
"1": "1 minuut",
|
||||
"3": "3 minuten"
|
||||
}
|
||||
},
|
||||
"cycle": {
|
||||
"name": "Programma"
|
||||
},
|
||||
@@ -189,6 +248,9 @@
|
||||
"8f": "Babyverzorging"
|
||||
}
|
||||
},
|
||||
"dispense_type": {
|
||||
"name": "Type dispenser"
|
||||
},
|
||||
"door_alert": {
|
||||
"name": "Deuralarm",
|
||||
"state": {
|
||||
@@ -201,6 +263,8 @@
|
||||
"dryer_cycle_table_03": {
|
||||
"name": "Programma",
|
||||
"state": {
|
||||
"01": "Normaal",
|
||||
"06": "Tijdprogramma",
|
||||
"16": "Katoen",
|
||||
"18": "Synthetisch",
|
||||
"19": "Fijne was",
|
||||
@@ -217,6 +281,12 @@
|
||||
"1f": "Gemengde was"
|
||||
}
|
||||
},
|
||||
"favorite_capacity": {
|
||||
"name": "Favoriete capaciteit"
|
||||
},
|
||||
"favorite_hotwater_temperature": {
|
||||
"name": "Favoriete temperatuur warm water"
|
||||
},
|
||||
"finish_sound": {
|
||||
"name": "Eindgeluid",
|
||||
"state": {
|
||||
@@ -248,6 +318,9 @@
|
||||
"extra_high": "Extra hoog"
|
||||
}
|
||||
},
|
||||
"hot_water_temperature": {
|
||||
"name": "Temperatuur warm water"
|
||||
},
|
||||
"ice_type": {
|
||||
"name": "Type {instance_name}",
|
||||
"state": {
|
||||
@@ -286,6 +359,9 @@
|
||||
"air_fry": "Airfryen"
|
||||
}
|
||||
},
|
||||
"operating_mode": {
|
||||
"name": "Bedrijfsmodus"
|
||||
},
|
||||
"pantry_zone_mode": {
|
||||
"name": "Modus voorraadzone",
|
||||
"state": {
|
||||
@@ -337,6 +413,13 @@
|
||||
"mute": "Dempen"
|
||||
}
|
||||
},
|
||||
"air_purifier_sound_mode": {
|
||||
"name": "Geluidsmodus",
|
||||
"state": {
|
||||
"mute": "Dempen",
|
||||
"buzzer": "Zoemer"
|
||||
}
|
||||
},
|
||||
"spin_speed": {
|
||||
"name": "Centrifugesnelheid",
|
||||
"state": {
|
||||
@@ -379,30 +462,37 @@
|
||||
"27": "Spoelen+centrifugeren",
|
||||
"28": "Afpompen/centrifugeren",
|
||||
"29": "Drum Clean+",
|
||||
"2a": "Spijkerbroek",
|
||||
"2b": "AI Wash",
|
||||
"2d": "Stille was",
|
||||
"2e": "Babyverzorging",
|
||||
"2f": "Sportkleding",
|
||||
"30": "Bewolkte dag",
|
||||
"32": "Overhemden",
|
||||
"33": "Beddengoed",
|
||||
"34": "Gemengd",
|
||||
"36": "Wassen+drogen",
|
||||
"37": "Air Wash",
|
||||
"38": "Katoen drogen",
|
||||
"39": "Synthetisch drogen",
|
||||
"3a": "Trommel reinigen",
|
||||
"52": "Eco koud",
|
||||
"53": "Intensief",
|
||||
"54": "Handdoeken",
|
||||
"55": "Sportkleding",
|
||||
"57": "Fijn",
|
||||
"5e": "Spoelen+centrifugeren",
|
||||
"60": "Zelfreinigend+",
|
||||
"65": "Bonte was",
|
||||
"66": "Spijkergoed",
|
||||
"7c": "Witte was",
|
||||
"7d": "Beddengoed/Waterdicht",
|
||||
"7e": "Zelfreinigend",
|
||||
"7f": "Wol/Fijn",
|
||||
"86": "Diep wassen",
|
||||
"87": "Downloaden",
|
||||
"8f": "Intensief koud",
|
||||
"96": "Minder microvezels",
|
||||
"2b": "AI Wash",
|
||||
"2a": "Spijkerbroek"
|
||||
"96": "Minder microvezels"
|
||||
}
|
||||
},
|
||||
"washer_dry_level": {
|
||||
@@ -441,6 +531,9 @@
|
||||
"clean_level": {
|
||||
"name": "Reinigingsniveau"
|
||||
},
|
||||
"coffee_brew_status": {
|
||||
"name": "Koffiezetstatus"
|
||||
},
|
||||
"connection_mode": {
|
||||
"name": "Verbindingsmodus",
|
||||
"state": {
|
||||
@@ -448,15 +541,58 @@
|
||||
"poll": "Polling"
|
||||
}
|
||||
},
|
||||
"cooktop_running_state": {
|
||||
"name": "Loopstatus kookplaat"
|
||||
},
|
||||
"cooktop_state": {
|
||||
"name": "Status kookplaat"
|
||||
},
|
||||
"current_limit_level": {
|
||||
"name": "Niveau stroombegrenzing"
|
||||
},
|
||||
"hepa_filter_usage": {
|
||||
"name": "HEPA-filtergebruik"
|
||||
},
|
||||
"panel_status": {
|
||||
"name": "Paneelstatus"
|
||||
},
|
||||
"sound_output": {
|
||||
"name": "Geluidsuitvoer"
|
||||
},
|
||||
"dustbag_usage": {
|
||||
"name": "Stofzakgebruik"
|
||||
},
|
||||
"cleanstation_status": {
|
||||
"name": "Status schoonmaakstation"
|
||||
},
|
||||
"stick_status": {
|
||||
"name": "Status steel"
|
||||
},
|
||||
"uvc_operation_time": {
|
||||
"name": "UV-C bedrijfstijd"
|
||||
},
|
||||
"uvc_total_operation_time": {
|
||||
"name": "UV-C totale bedrijfstijd"
|
||||
},
|
||||
"uvc_finished_time": {
|
||||
"name": "UV-C voltooid op"
|
||||
},
|
||||
"uvc_emitted_time": {
|
||||
"name": "UV-C uitgestraald op"
|
||||
},
|
||||
"overload_protection_mode": {
|
||||
"name": "Modus overbelastingsbeveiliging",
|
||||
"state": {
|
||||
"alarm": "Alarm",
|
||||
"powersaving": "Energiebesparing"
|
||||
}
|
||||
},
|
||||
"current_temp_c": {
|
||||
"name": "Temperatuur"
|
||||
},
|
||||
"current_temperature_c": {
|
||||
"name": "Temperatuur"
|
||||
},
|
||||
"diagnosis": {
|
||||
"name": "Diagnose"
|
||||
},
|
||||
@@ -499,6 +635,9 @@
|
||||
"fan_speed_level": {
|
||||
"name": "Ventilatorniveau"
|
||||
},
|
||||
"filter_clean_remain_time": {
|
||||
"name": "Resterende tijd filterreiniging"
|
||||
},
|
||||
"filter_progress": {
|
||||
"name": "Filterverbruik"
|
||||
},
|
||||
@@ -523,9 +662,15 @@
|
||||
"hood_filter_capacity": {
|
||||
"name": "Filtercapaciteit"
|
||||
},
|
||||
"humidity": {
|
||||
"name": "Luchtvochtigheid"
|
||||
},
|
||||
"hood_filter_usage": {
|
||||
"name": "Filterverbruik"
|
||||
},
|
||||
"humidity": {
|
||||
"name": "Luchtvochtigheid"
|
||||
},
|
||||
"instance_temperature": {
|
||||
"name": "Temperatuur {instance_name}"
|
||||
},
|
||||
@@ -574,6 +719,15 @@
|
||||
"power_watts": {
|
||||
"name": "Vermogen"
|
||||
},
|
||||
"probe_battery": {
|
||||
"name": "Sonde batterij"
|
||||
},
|
||||
"probe_target_temperature": {
|
||||
"name": "Doeltemperatuur sonde"
|
||||
},
|
||||
"probe_temperature": {
|
||||
"name": "Temperatuur sonde"
|
||||
},
|
||||
"progress": {
|
||||
"name": "Voortgang"
|
||||
},
|
||||
@@ -589,12 +743,30 @@
|
||||
"selfcheck_status": {
|
||||
"name": "Status zelfcontrole"
|
||||
},
|
||||
"sterilize_last_time": {
|
||||
"name": "Laatste sterilisatie"
|
||||
},
|
||||
"sterilize_period": {
|
||||
"name": "Sterilisatie-interval"
|
||||
},
|
||||
"sterilize_plan_time": {
|
||||
"name": "Geplande sterilisatie"
|
||||
},
|
||||
"sterilize_run_time": {
|
||||
"name": "Sterilisatieduur"
|
||||
},
|
||||
"super_fine_dust": {
|
||||
"name": "Ultrafijnstof"
|
||||
},
|
||||
"warming_center_state": {
|
||||
"name": "Status warmhoudzone"
|
||||
},
|
||||
"water_liters": {
|
||||
"name": "Waterverbruik"
|
||||
},
|
||||
"waterpurifier_status": {
|
||||
"name": "Status"
|
||||
},
|
||||
"machine_state": {
|
||||
"name": "Machinestatus",
|
||||
"state": {
|
||||
@@ -629,6 +801,21 @@
|
||||
"auto_clean": {
|
||||
"name": "Automatisch reinigen"
|
||||
},
|
||||
"display": {
|
||||
"name": "Display"
|
||||
},
|
||||
"pet_filter_activation": {
|
||||
"name": "Huisdierfilter actief"
|
||||
},
|
||||
"auto_empty": {
|
||||
"name": "Automatisch legen"
|
||||
},
|
||||
"dustbin_auto_close": {
|
||||
"name": "Stofreservoir automatisch sluiten"
|
||||
},
|
||||
"uvc_intensive_mode": {
|
||||
"name": "Intensieve UV-C-modus"
|
||||
},
|
||||
"auto_door_opener": {
|
||||
"name": "Automatische deuropener"
|
||||
},
|
||||
@@ -641,6 +828,9 @@
|
||||
"bubble_soak": {
|
||||
"name": "Bubble Soak"
|
||||
},
|
||||
"buzz_lock": {
|
||||
"name": "Zoemerslot"
|
||||
},
|
||||
"cabinet_light_dim": {
|
||||
"name": "Geleidelijk feller"
|
||||
},
|
||||
@@ -650,6 +840,12 @@
|
||||
"child_lock": {
|
||||
"name": "Kinderslot"
|
||||
},
|
||||
"coldwater_lock": {
|
||||
"name": "Koudwaterslot"
|
||||
},
|
||||
"cooktop_child_lock": {
|
||||
"name": "Kinderslot"
|
||||
},
|
||||
"defrost_delay": {
|
||||
"name": "Ontdooien uitstellen"
|
||||
},
|
||||
@@ -659,9 +855,21 @@
|
||||
"fast_preheat": {
|
||||
"name": "Snel voorverwarmen"
|
||||
},
|
||||
"favorite_capacity_enabled": {
|
||||
"name": "Favoriete capaciteit"
|
||||
},
|
||||
"favorite_coffee_enabled": {
|
||||
"name": "Favoriete koffie"
|
||||
},
|
||||
"favorite_hotwater_enabled": {
|
||||
"name": "Favoriet warm water"
|
||||
},
|
||||
"fridge_sound": {
|
||||
"name": "Geluid"
|
||||
},
|
||||
"hotwater_lock": {
|
||||
"name": "Heetwaterslot"
|
||||
},
|
||||
"ice_maker_enabled": {
|
||||
"name": "IJsmaker"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,466 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/airlevelcheck/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sensingState": "NonProcessing",
|
||||
"x.com.samsung.da.lastSensingTime": "1785157841",
|
||||
"x.com.samsung.da.lastSensingLevel": "Kr1",
|
||||
"x.com.samsung.da.periodicSensingSkipStatus": "Off",
|
||||
"x.com.samsung.da.periodicSensingSkipTime": "00000000",
|
||||
"x.com.samsung.da.periodicSensingActivationState": "On",
|
||||
"x.com.samsung.da.autoExeState": "Off",
|
||||
"x.com.samsung.da.supportedAutoExeState": [
|
||||
"Off",
|
||||
"Airpurify",
|
||||
"Alarm"
|
||||
],
|
||||
"x.com.samsung.da.startSensingOnce": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-27T13:18:51",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
}
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.alarms"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/availablecontrolsets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "3B000100000A0000000000000000",
|
||||
"x.com.samsung.da.id": "VTL",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "2650000000",
|
||||
"x.com.samsung.da.airconOptionList": [
|
||||
"AI_PURIFY",
|
||||
"WELCOMECARE",
|
||||
"DR"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/da/softreset/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.softwarereset": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/devicespecificinfo/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.deviceActive": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/display/vs/0",
|
||||
"rep": {
|
||||
"mode": "On",
|
||||
"supportedModes": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/dnd/autosleep/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.visible": "true",
|
||||
"x.com.samsung.da.startTime": "00:00:00",
|
||||
"x.com.samsung.da.endTime": "00:00:00",
|
||||
"x.com.samsung.da.useTimeSetting": "false",
|
||||
"x.com.samsung.da.functionState": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.start": "1970-01-01T00:00:00Z",
|
||||
"x.com.samsung.da.override": "Off",
|
||||
"x.com.samsung.da.durationminutes": "0",
|
||||
"x.com.samsung.da.drlcLevel": "0",
|
||||
"x.com.samsung.da.realSaving": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cumulativeDate": "1785158301",
|
||||
"x.com.samsung.da.cumulativeSavedPower": "0",
|
||||
"x.com.samsung.da.cumulativePower": "32428",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePowerType": "total"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+09:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/hepafilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsage": "70",
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterCapacity": "8760",
|
||||
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"replaceable"
|
||||
],
|
||||
"x.com.samsung.da.filterStatus": "normal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.modelNum": "TP1X_DA-AC-AIR-01031_0000|10268241|70000721001813CC4D1A490301082100",
|
||||
"x.com.samsung.da.description": "TP1X_DA-AC-AIR-01031_0000",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02660A260401",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.newVersionAvailable": "0",
|
||||
"x.com.samsung.da.number": "02682A25042102,02683A25031801"
|
||||
}
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "AP2",
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagTsId": "DA01"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/keepnormalstate/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.keepnormal": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.kidsLock": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/light/vs/0",
|
||||
"rep": {
|
||||
"mode": "On",
|
||||
"supportedModes": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": [
|
||||
"Smart"
|
||||
],
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Smart",
|
||||
"Max",
|
||||
"Mid",
|
||||
"WindFree",
|
||||
"Sleep"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"OptionCode_23418",
|
||||
"SmartSleep_Off",
|
||||
"WelcomeCare_On",
|
||||
"Pollution_Off",
|
||||
"ProtectOperation_Off",
|
||||
"UpdateAllow_NotAllowed"
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.mode"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/muteonce/vs/0",
|
||||
"rep": {
|
||||
"muteonce": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/panel/vs/0",
|
||||
"rep": {
|
||||
"status": "Close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/personality/presence/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/petfilteractivation/vs/0",
|
||||
"rep": {
|
||||
"status": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "On",
|
||||
"operationNumber": "36",
|
||||
"displayCondition": "Enable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.longnotisubscription": "true",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/reserverulesets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "3F00000000000000000000",
|
||||
"x.com.samsung.da.id": "VTL",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sensors/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cleanLevel": "1",
|
||||
"x.com.samsung.da.total": "5",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Sensor for CleanLevel",
|
||||
"x.com.samsung.da.type": "CleanLevel",
|
||||
"x.com.samsung.da.value": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Sensor for Odor",
|
||||
"x.com.samsung.da.type": "Odor",
|
||||
"x.com.samsung.da.value": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Sensor for Dust",
|
||||
"x.com.samsung.da.type": "Dust",
|
||||
"x.com.samsung.da.value": [
|
||||
"5",
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "3",
|
||||
"x.com.samsung.da.description": "Sensor for FineDust",
|
||||
"x.com.samsung.da.type": "FineDust",
|
||||
"x.com.samsung.da.value": [
|
||||
"5",
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "4",
|
||||
"x.com.samsung.da.description": "Sensor for SuperFineDust",
|
||||
"x.com.samsung.da.type": "SuperFineDust",
|
||||
"x.com.samsung.da.value": [
|
||||
"5",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.sensors"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.s"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/settings/sound/mode/vs/0",
|
||||
"rep": {
|
||||
"mode": "mute",
|
||||
"supportedModes": [
|
||||
"mute",
|
||||
"buzzer"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/settings/sound/output/vs/0",
|
||||
"rep": {
|
||||
"deviceType": "buzzer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/settings/sound/volume/vs/0",
|
||||
"rep": {
|
||||
"level": "0",
|
||||
"minLevel": "0",
|
||||
"maxLevel": "3",
|
||||
"resolution": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "Micom",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"x.com.samsung.da.newVersionNo": "12250421",
|
||||
"x.com.samsung.da.currentVersionInfo": "10000000",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "0",
|
||||
"otnTarget": "main",
|
||||
"otnCompleteDate": "noHistory",
|
||||
"scheduledTime": "None",
|
||||
"swVersionInfo": {
|
||||
"platform": "Tizen Lite",
|
||||
"oneUiVersion": "7.0 Air purifier",
|
||||
"osVersion": "4.0"
|
||||
},
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "AVT-KR-TP1-24-AXXX00",
|
||||
"versions": [
|
||||
"12260401"
|
||||
],
|
||||
"visVersion": "260401"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "895010268241FFFFFFFF",
|
||||
"versions": [
|
||||
"25042102",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "250421"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "09501026824110268341",
|
||||
"versions": [
|
||||
"25042102",
|
||||
"25031801"
|
||||
],
|
||||
"visVersion": "250421"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Seoul",
|
||||
"offset": "+09:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connectionconfig/vs/0",
|
||||
"rep": {
|
||||
"autoReconnectionMinVersion": "1.0",
|
||||
"autoReconnection": "true",
|
||||
"autoReconnectionProtocolType": [
|
||||
"helper_hotspot",
|
||||
"ble_ocf"
|
||||
],
|
||||
"supportedWiFiAuthType": [
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA-PSK",
|
||||
"WPA2-PSK",
|
||||
"SAE"
|
||||
],
|
||||
"supportedWiFiCryptoType": [
|
||||
"TKIP",
|
||||
"AES",
|
||||
"WEP-64",
|
||||
"WEP-128"
|
||||
],
|
||||
"supportedWiFiFreq": [
|
||||
"2.4G"
|
||||
],
|
||||
"calmConnectionCare": {
|
||||
"version": "1.0",
|
||||
"role": [
|
||||
"things"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wirelessinfo/vs/0",
|
||||
"rep": {
|
||||
"macaddressWiFi": "**REDACTED**",
|
||||
"macaddressBLE": "**REDACTED**",
|
||||
"connectedApSsid": "Branden_Summit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/quickcontrol/info/vs/0",
|
||||
"rep": {
|
||||
"supportedVersion": "1.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/personality/presence/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "",
|
||||
"x.com.samsung.da.deviceId": "**REDACTED**",
|
||||
"x.com.samsung.da.value": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.longnotisubscription": "false",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/airdustfilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsage": "54",
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterDesiredUsage": "500",
|
||||
"x.com.samsung.da.filterStatus": "normal",
|
||||
"x.com.samsung.da.filterCapacity": "500",
|
||||
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"replaceable",
|
||||
"washable"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/control/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.increment": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/convenient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Off",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Off",
|
||||
"Sleep",
|
||||
"Quiet",
|
||||
"Smart",
|
||||
"Speed",
|
||||
"Nano",
|
||||
"NanoSleep"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/autoclean/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Stop",
|
||||
"x.com.samsung.da.settingStatus": "Off",
|
||||
"x.com.samsung.da.progress": "0",
|
||||
"x.com.samsung.da.supportedStatus": [
|
||||
"Start",
|
||||
"Stop"
|
||||
],
|
||||
"x.com.samsung.da.supportedSettingStatus": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/strength/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "0",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4"
|
||||
],
|
||||
"x.com.samsung.da.modesName": [
|
||||
"Auto",
|
||||
"Low",
|
||||
"Mid",
|
||||
"High",
|
||||
"Turbo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/direction/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Left_And_Right",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Fix",
|
||||
"Up_And_Low",
|
||||
"Left_And_Right",
|
||||
"All"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-27T06:07:25",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "FilterAlarm_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-27T06:07:25",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Temperature",
|
||||
"x.com.samsung.da.desired": "26.0",
|
||||
"x.com.samsung.da.current": "30.0",
|
||||
"x.com.samsung.da.maximum": "30",
|
||||
"x.com.samsung.da.minimum": "18",
|
||||
"x.com.samsung.da.increment": "1.0",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/current/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
16,
|
||||
30
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 30.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
16,
|
||||
30
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 26.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "0.000000",
|
||||
"x.com.samsung.da.cumulativePower": "3193591",
|
||||
"x.com.samsung.da.cumulativeSavedPower": "158331",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {
|
||||
"power": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Auto",
|
||||
"Cool",
|
||||
"Dry",
|
||||
"Wind"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"Dry"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"Sleep_0",
|
||||
"ArtificialWorking_Off",
|
||||
"ComfortAICooling_Off",
|
||||
"AiTempChanged_Off",
|
||||
"AiTemp_270",
|
||||
"OutdoorConnection_Connected",
|
||||
"OutdoorTemp_88",
|
||||
"CoolCapa_53",
|
||||
"WarmCapa_0",
|
||||
"Light_Off",
|
||||
"Volume_100",
|
||||
"StopAutoClean_Idle",
|
||||
"Autoclean_Off",
|
||||
"DiagnosisAI_Off",
|
||||
"ProgressDiagnosisAI_1",
|
||||
"ResultDiagnosisAI_Normal",
|
||||
"SmartCoolClean_Off",
|
||||
"ProgressSmartClean_0",
|
||||
"FreezeAlarmSetting_On",
|
||||
"DesiredFreezeAlarm_112",
|
||||
"WashAlarm_Off",
|
||||
"OptionCode_56440",
|
||||
"ExtendOptionCode_246413",
|
||||
"RacInfo_None",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"DurationOn_0",
|
||||
"WelcomeCoolingState_On"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "Off",
|
||||
"x.com.samsung.da.displaycondition": "Enable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sensors/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "ARA-WW-TP1-22-COMMON|10229641|60010535001511014600083200800000",
|
||||
"x.com.samsung.da.description": "ARA-WW-TP1-22-COMMON",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "010",
|
||||
"x.com.samsung.da.diagMinVersion": "1.0",
|
||||
"x.com.samsung.da.diagTsId": "DA01",
|
||||
"x.com.samsung.da.serialNumOption": "**REDACTED**",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02378A260327",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "102296A23012700",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "102295A22113000,102299A10000300",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+08:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "0000000000",
|
||||
"x.com.samsung.da.airconOptionList": [
|
||||
"SingleCommand_1",
|
||||
"DR",
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"PRODUCT_GLOBAL",
|
||||
"AI_RAC_GLOBAL_COOLONLY_3.0",
|
||||
"AI_3.0",
|
||||
"Auto_To_AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/0",
|
||||
"rep": {
|
||||
"humidity": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.humidity": "0",
|
||||
"x.com.samsung.da.fivepercentHumidity": "64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/0",
|
||||
"rep": {
|
||||
"DRLevel": 1,
|
||||
"start": "2026-07-27T05:07:32Z",
|
||||
"duration": 20,
|
||||
"override": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.drlcLevel": "1",
|
||||
"x.com.samsung.da.duration": "20:47:00",
|
||||
"x.com.samsung.da.drlcStartTime": "2026-07-27T05:07:32Z",
|
||||
"x.com.samsung.da.override": "Off",
|
||||
"x.com.samsung.da.realSaving": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/availablecontrolsets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "000000B4012C016102490C000000",
|
||||
"x.com.samsung.da.id": "RAC",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/keepnormalstate/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.keepnormal": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"x.com.samsung.da.newVersionNo": "00000000",
|
||||
"x.com.samsung.da.currentVersionInfo": "00000000",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "",
|
||||
"otnTarget": "main",
|
||||
"otnCompleteDate": "noHistory",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "ARA-WW-TP1-22-COMMON",
|
||||
"versions": [
|
||||
"11260327"
|
||||
],
|
||||
"visVersion": "260327"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "",
|
||||
"versions": [
|
||||
"23012700",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "230127"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "",
|
||||
"versions": [
|
||||
"23012700",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "230127"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connectionconfig/vs/0",
|
||||
"rep": {
|
||||
"autoReconnectionMinVersion": "1.0",
|
||||
"autoReconnection": "true",
|
||||
"autoReconnectionProtocolType": [
|
||||
"helper_hotspot",
|
||||
"ble_ocf"
|
||||
],
|
||||
"supportedWiFiAuthType": [
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA-PSK",
|
||||
"WPA2-PSK",
|
||||
"SAE"
|
||||
],
|
||||
"supportedWiFiCryptoType": [
|
||||
"TKIP",
|
||||
"AES",
|
||||
"WEP-64",
|
||||
"WEP-128"
|
||||
],
|
||||
"supportedWiFiFreq": [
|
||||
"2.4G"
|
||||
],
|
||||
"calmConnectionCare": {
|
||||
"version": "1.0",
|
||||
"role": [
|
||||
"things"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Kuala_Lumpur",
|
||||
"offset": "+08:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/muteonce/vs/0",
|
||||
"rep": {
|
||||
"muteonce": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/aisleep/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.displayNightMode": "Off",
|
||||
"x.com.samsung.da.elapsedTime": "0",
|
||||
"x.com.samsung.da.requestFeedback": "Off",
|
||||
"x.com.samsung.da.resultFeedback": "0",
|
||||
"x.com.samsung.da.statusFeedback": "Idle",
|
||||
"x.com.samsung.da.sleepTime": "14002200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wirelessinfo/vs/0",
|
||||
"rep": {
|
||||
"macaddressWiFi": "**REDACTED**",
|
||||
"macaddressBLE": "**REDACTED**",
|
||||
"connectedApSsid": "dreamer"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
{
|
||||
"meta": {
|
||||
"model": "TP1X_DA-AC-RAC-01001_0000",
|
||||
"device_type": "airconditioner",
|
||||
"source": "user diagnostics (scrubbed)",
|
||||
"note": "Cool-only global TP1X RAC variant (airconOptionList AI_RAC_GLOBAL_COOLONLY_3.0). Unlike the other TP1X_DA-AC-RAC dumps, its /otninformation/vs/0 ships no swVersionInfo block, so oneUiVersion is empty and detection must fall back to the hyphenated '-RAC-' modelNum token in for_device_by_model. Adds two hrefs absent from the other AC dumps -- /stepcontrol/vs/0 (manual airflow step) and /remotedeviceinfo/vs/0 (empty paired-device list), both ignored -- and exposes Samsung WindFree via the convenient-mode codes Nano/NanoSleep (in place of the Smart code on other boards; confirmed by an off-vs-on dump diff)."
|
||||
},
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/personality/presence/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "",
|
||||
"x.com.samsung.da.deviceId": "**REDACTED**",
|
||||
"x.com.samsung.da.value": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.longnotisubscription": "false",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/airdustfilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsage": "100",
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterDesiredUsage": "500",
|
||||
"x.com.samsung.da.filterStatus": "wash",
|
||||
"x.com.samsung.da.filterCapacity": "500",
|
||||
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"replaceable",
|
||||
"washable"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/control/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.increment": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/convenient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Off",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Off",
|
||||
"Sleep",
|
||||
"Quiet",
|
||||
"Speed",
|
||||
"Nano",
|
||||
"NanoSleep"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/autoclean/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Stop",
|
||||
"x.com.samsung.da.settingStatus": "Off",
|
||||
"x.com.samsung.da.progress": "0",
|
||||
"x.com.samsung.da.supportedStatus": [
|
||||
"Start",
|
||||
"Stop"
|
||||
],
|
||||
"x.com.samsung.da.supportedSettingStatus": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/strength/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "0",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4"
|
||||
],
|
||||
"x.com.samsung.da.modesName": [
|
||||
"Auto",
|
||||
"Low",
|
||||
"Mid",
|
||||
"High",
|
||||
"Turbo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/direction/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Fix",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Fix",
|
||||
"Up_And_Low",
|
||||
"Left_And_Right",
|
||||
"All"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-26T15:24:44",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "FilterAlarm",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-26T15:24:44",
|
||||
"x.com.samsung.da.state": "Created"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Temperature",
|
||||
"x.com.samsung.da.desired": "25.0",
|
||||
"x.com.samsung.da.current": "20.0",
|
||||
"x.com.samsung.da.maximum": "30",
|
||||
"x.com.samsung.da.minimum": "16",
|
||||
"x.com.samsung.da.increment": "1.0",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/current/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
16,
|
||||
30
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 20.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
16,
|
||||
30
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 25.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "0.000000",
|
||||
"x.com.samsung.da.cumulativePower": "445938",
|
||||
"x.com.samsung.da.cumulativeSavedPower": "81528",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {
|
||||
"power": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Auto",
|
||||
"Cool",
|
||||
"Dry",
|
||||
"Wind"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"Auto"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"Sleep_0",
|
||||
"ArtificialWorking_Off",
|
||||
"ComfortAICooling_Off",
|
||||
"AiTempChanged_Off",
|
||||
"AiTemp_240",
|
||||
"OutdoorTemp_84",
|
||||
"CoolCapa_35",
|
||||
"WarmCapa_0",
|
||||
"Light_Off",
|
||||
"Volume_Mute",
|
||||
"StopAutoClean_Idle",
|
||||
"Autoclean_Off",
|
||||
"DiagnosisAI_Off",
|
||||
"ProgressDiagnosisAI_0",
|
||||
"ResultDiagnosisAI_Normal",
|
||||
"OptionCode_52344",
|
||||
"ExtendOptionCode_230029",
|
||||
"RacInfo_None",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"DurationOn_0",
|
||||
"WelcomeCoolingState_Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "Off",
|
||||
"causeSource": "DEFT",
|
||||
"operationNumber": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sensors/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "TP1X_DA-AC-RAC-01001_0000|10247441|60010523001811014E00002200D00000",
|
||||
"x.com.samsung.da.description": "TP1X_DA-AC-RAC-01001_0000",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "AR2",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagTsId": "DA01",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02646A260327",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "102474A23112200",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "102702A24041900,102579A10000200",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "-03:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "0000000000",
|
||||
"x.com.samsung.da.airconOptionList": [
|
||||
"SingleCommand_1",
|
||||
"DR",
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"PRODUCT_GLOBAL",
|
||||
"AI_RAC_GLOBAL_COOLONLY_3.0",
|
||||
"AI_3.0",
|
||||
"Auto_To_AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/0",
|
||||
"rep": {
|
||||
"humidity": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.humidity": "0",
|
||||
"x.com.samsung.da.fivepercentHumidity": "66"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/0",
|
||||
"rep": {
|
||||
"DRLevel": 1,
|
||||
"start": "2026-07-26T12:01:41Z",
|
||||
"duration": 23,
|
||||
"override": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.drlcLevel": "1",
|
||||
"x.com.samsung.da.duration": "23:59:00",
|
||||
"x.com.samsung.da.drlcStartTime": "2026-07-26T12:01:41Z",
|
||||
"x.com.samsung.da.override": "Off",
|
||||
"x.com.samsung.da.realSaving": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/availablecontrolsets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "000000A0012C0161024904000000",
|
||||
"x.com.samsung.da.id": "RAC",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/stepcontrol/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Off",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Off",
|
||||
"80",
|
||||
"60",
|
||||
"40",
|
||||
"Power"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/keepnormalstate/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.keepnormal": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotedatacontrol/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Off",
|
||||
"x.com.samsung.da.connectionStatus": "Disconnected"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotetemperature/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.temperature": "",
|
||||
"x.com.samsung.da.unit": "",
|
||||
"x.com.samsung.da.error": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotedeviceinfo/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.didList": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"x.com.samsung.da.newVersionNo": "00000000",
|
||||
"x.com.samsung.da.currentVersionInfo": "00000000",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "",
|
||||
"otnTarget": "main",
|
||||
"otnCompleteDate": "noHistory",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "ARA-WW-TP1-24-ARXX00",
|
||||
"versions": [
|
||||
"11260327"
|
||||
],
|
||||
"visVersion": "260327"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "045210247441FFFFFFFF",
|
||||
"versions": [
|
||||
"23112200",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "231122"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "04521027024110257941",
|
||||
"versions": [
|
||||
"24041900",
|
||||
"10000200"
|
||||
],
|
||||
"visVersion": "240419"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connectionconfig/vs/0",
|
||||
"rep": {
|
||||
"autoReconnectionMinVersion": "1.0",
|
||||
"autoReconnection": "true",
|
||||
"autoReconnectionProtocolType": [
|
||||
"helper_hotspot",
|
||||
"ble_ocf"
|
||||
],
|
||||
"supportedWiFiAuthType": [
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA-PSK",
|
||||
"WPA2-PSK",
|
||||
"SAE"
|
||||
],
|
||||
"supportedWiFiCryptoType": [
|
||||
"TKIP",
|
||||
"AES",
|
||||
"WEP-64",
|
||||
"WEP-128"
|
||||
],
|
||||
"supportedWiFiFreq": [
|
||||
"2.4G"
|
||||
],
|
||||
"calmConnectionCare": {
|
||||
"version": "1.0",
|
||||
"role": [
|
||||
"things"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "America/Sao_Paulo",
|
||||
"offset": "-03:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/muteonce/vs/0",
|
||||
"rep": {
|
||||
"muteonce": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/aisleep/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.displayNightMode": "Off",
|
||||
"x.com.samsung.da.elapsedTime": "0",
|
||||
"x.com.samsung.da.requestFeedback": "Off",
|
||||
"x.com.samsung.da.resultFeedback": "0",
|
||||
"x.com.samsung.da.statusFeedback": "Idle",
|
||||
"x.com.samsung.da.sleepTime": "14002200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wirelessinfo/vs/0",
|
||||
"rep": {
|
||||
"macaddressWiFi": "**REDACTED**",
|
||||
"macaddressBLE": "**REDACTED**",
|
||||
"connectedApSsid": "**REDACTED**"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/quickcontrol/info/vs/0",
|
||||
"rep": {
|
||||
"supportedVersion": "1.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-25T08:19:03"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "FilterAlarm_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-25T08:19:03"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "0000000000",
|
||||
"x.com.samsung.da.airconOptionList": [
|
||||
"SingleCommand_1",
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"PRODUCT_GLOBAL",
|
||||
"AI_RAC_GLOBAL_2.0",
|
||||
"AI_2.0",
|
||||
"Auto_To_AI",
|
||||
"AI_Heat"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/diagnosis/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {
|
||||
"power": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "0",
|
||||
"x.com.samsung.da.cumulativePower": "320466",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+00:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/airdustfilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsage": "41",
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterDesiredUsage": "500",
|
||||
"x.com.samsung.da.filterStatus": "normal",
|
||||
"x.com.samsung.da.filterCapacity": "500",
|
||||
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"replaceable",
|
||||
"washable"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/0",
|
||||
"rep": {
|
||||
"humidity": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.humidity": "0",
|
||||
"x.com.samsung.da.fivepercentHumidity": "42"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "ARTIK051_PRAC_20K|10217841|60010519001411010200001000000000",
|
||||
"x.com.samsung.da.description": "ARTIK051_PRAC_20K",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.serialNumOption": "**REDACTED**",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02181A230313",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "20082000,FFFFFFFF",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Outdoor",
|
||||
"x.com.samsung.da.number": "20091600,10000400"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/convenient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Off",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Off",
|
||||
"Sleep",
|
||||
"Quiet",
|
||||
"Smart",
|
||||
"Speed",
|
||||
"MotionIndirect",
|
||||
"MotionDirect",
|
||||
"Nano",
|
||||
"NanoSleep"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Cool",
|
||||
"Dry",
|
||||
"Wind",
|
||||
"Auto",
|
||||
"Heat"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"Dry"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"Sleep_0",
|
||||
"ArtificialWorking_Off",
|
||||
"ComfortAICooling_Off",
|
||||
"MotionState_Normal",
|
||||
"AiTempChanged_Off",
|
||||
"AiTemp_240",
|
||||
"OutdoorTemp_87",
|
||||
"CoolCapa_25",
|
||||
"WarmCapa_32",
|
||||
"Light_Off",
|
||||
"Volume_100",
|
||||
"MotionMode_Default",
|
||||
"OptionCode_56376",
|
||||
"ExtendOptionCode_781",
|
||||
"RacInfo_None",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"DurationOn_0",
|
||||
"WelcomeCoolingState_Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/airpurify/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Off",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/autoclean/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Stop",
|
||||
"x.com.samsung.da.settingStatus": "On",
|
||||
"x.com.samsung.da.progress": "0",
|
||||
"x.com.samsung.da.supportedStatus": [
|
||||
"Start",
|
||||
"Stop"
|
||||
],
|
||||
"x.com.samsung.da.supportedSettingStatus": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/personality/presence/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "",
|
||||
"x.com.samsung.da.deviceId": "**REDACTED**",
|
||||
"x.com.samsung.da.value": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.longnotisubscription": "false",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sensors/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Sensor for CleanLevel",
|
||||
"x.com.samsung.da.type": "CleanLevel",
|
||||
"x.com.samsung.da.value": [
|
||||
"0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Sensor for Odor",
|
||||
"x.com.samsung.da.type": "Odor",
|
||||
"x.com.samsung.da.value": [
|
||||
"0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Sensor for Dust",
|
||||
"x.com.samsung.da.type": "Dust",
|
||||
"x.com.samsung.da.value": [
|
||||
"0",
|
||||
"0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "3",
|
||||
"x.com.samsung.da.description": "Sensor for FineDust",
|
||||
"x.com.samsung.da.type": "FineDust",
|
||||
"x.com.samsung.da.value": [
|
||||
"0",
|
||||
"0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "4",
|
||||
"x.com.samsung.da.description": "Sensor for SuperFineDust",
|
||||
"x.com.samsung.da.type": "SuperFineDust",
|
||||
"x.com.samsung.da.value": [
|
||||
"0",
|
||||
"0"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/control/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.increment": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/current/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
16.0,
|
||||
30.0
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 27.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
16.0,
|
||||
30.0
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 26.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Temperature",
|
||||
"x.com.samsung.da.desired": "26.0",
|
||||
"x.com.samsung.da.current": "27.0",
|
||||
"x.com.samsung.da.maximum": "30",
|
||||
"x.com.samsung.da.minimum": "18",
|
||||
"x.com.samsung.da.increment": "1.0",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/wind/direction/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Fix",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Fix",
|
||||
"All",
|
||||
"Up_And_Low",
|
||||
"Left_And_Right"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/strength/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "0",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4"
|
||||
],
|
||||
"x.com.samsung.da.modesName": [
|
||||
"Auto",
|
||||
"Low",
|
||||
"Mid",
|
||||
"High",
|
||||
"Turbo"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/aisleep/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.displayNightMode": "Off",
|
||||
"x.com.samsung.da.elapsedTime": "0",
|
||||
"x.com.samsung.da.requestFeedback": "Off",
|
||||
"x.com.samsung.da.resultFeedback": 0,
|
||||
"x.com.samsung.da.statusFeedback": "Idle",
|
||||
"x.com.samsung.da.sleepTime": "14002200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-27T11:59:06",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
}
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.alarms"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/anomalyload/vs/0",
|
||||
"rep": {
|
||||
"operation": "Off",
|
||||
"mode": "Alarm",
|
||||
"supportedModes": [
|
||||
"Alarm",
|
||||
"PowerSaving"
|
||||
],
|
||||
"savingTime": "20",
|
||||
"supportedSavingTime": [
|
||||
"20",
|
||||
"40",
|
||||
"60"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/availablecontrolsets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "000000A0012C01610CC90C0F0000",
|
||||
"x.com.samsung.da.id": "RAC",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "0000000000",
|
||||
"x.com.samsung.da.airconOptionList": [
|
||||
"PRODUCT_GLOBAL",
|
||||
"AI_GLOBAL_COOLONLY_4.0",
|
||||
"AI_3.0",
|
||||
"Auto_To_AI",
|
||||
"ENERGY_2.0",
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"DR",
|
||||
"SingleCommand_1"
|
||||
],
|
||||
"InstalledRoomSize": "0.0",
|
||||
"SkipInstalledRoomSize": "false",
|
||||
"RoomSizeMenuNumber": "Manual"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/da/softreset/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.softwarereset": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.start": "1970-01-01T00:00:00Z",
|
||||
"x.com.samsung.da.override": "Off",
|
||||
"x.com.samsung.da.durationminutes": "0",
|
||||
"x.com.samsung.da.drlcLevel": "0",
|
||||
"x.com.samsung.da.realSaving": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cumulativeDate": "1785153515",
|
||||
"x.com.samsung.da.cumulativeSavedPower": "0",
|
||||
"x.com.samsung.da.cumulativePower": "47536",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePowerType": "total"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+08:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/airdustfilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterCapacity": "180",
|
||||
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"replaceable",
|
||||
"washable"
|
||||
],
|
||||
"x.com.samsung.da.filterStatus": "normal",
|
||||
"x.com.samsung.da.filterUsage": "44",
|
||||
"x.com.samsung.da.filterDesiredUsage": "180",
|
||||
"x.com.samsung.da.supportedFilterDesiredUsage": [
|
||||
"180",
|
||||
"300",
|
||||
"500",
|
||||
"700"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.humidity": "0",
|
||||
"x.com.samsung.da.fivepercentHumidity": "46"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.modelNum": "TP1X_DA-AC-RAC-01011_0000|10281241|6001051A001911018E0049300002A900",
|
||||
"x.com.samsung.da.description": "TP1X_DA-AC-RAC-01011_0000",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.serialNumOption": "**REDACTED**",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02825A260401",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.newVersionAvailable": "0",
|
||||
"x.com.samsung.da.number": "02812A25111800,FFFFFFFFFFFFFF"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.newVersionAvailable": "0",
|
||||
"x.com.samsung.da.number": "02813A25101500,FFFFFFFFFFFFFF"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "3",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.newVersionAvailable": "0",
|
||||
"x.com.samsung.da.number": "FFFFFFFFFFFFFF,FFFFFFFFFFFFFF"
|
||||
}
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "AR2",
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagTsId": "DA01"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/keepnormalstate/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.keepnormal": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/light/vs/0",
|
||||
"rep": {
|
||||
"mode": "On",
|
||||
"supportedModes": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mds/absencemonitoring/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/mds/absencestate/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/mode/convenient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Off",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Off",
|
||||
"Sleep",
|
||||
"Quiet",
|
||||
"Smart",
|
||||
"Speed",
|
||||
"Nano",
|
||||
"NanoSleep",
|
||||
"DryComfort"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": [
|
||||
"Cool"
|
||||
],
|
||||
"workingMode": "Cool",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Auto",
|
||||
"Cool",
|
||||
"Dry",
|
||||
"Fan"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"OptionCode_56416",
|
||||
"Sleep_0",
|
||||
"SleepRemainTime_0",
|
||||
"ArtificialWorking_Off",
|
||||
"ComfortAICooling_Off",
|
||||
"AiTempChanged_Off",
|
||||
"AiTemp_240",
|
||||
"Volume_100",
|
||||
"DiagnosisAI_Off",
|
||||
"ProgressDiagnosisAI_0",
|
||||
"ResultDiagnosisAI_Normal",
|
||||
"SmartCoolClean_Off",
|
||||
"ProgressSmartClean_0",
|
||||
"OutDoorVentil_Off",
|
||||
"InDoorVentil_Off",
|
||||
"FreezeAlarmSetting_Off",
|
||||
"DesiredFreezeAlarm_240",
|
||||
"WashAlarm_Off",
|
||||
"ExtendOptionCode_246413",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"DurationOn_0",
|
||||
"WelcomeCoolingState_Off"
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.mode"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/autoclean/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Stop",
|
||||
"x.com.samsung.da.supportedStatus": [
|
||||
"Start",
|
||||
"Stop"
|
||||
],
|
||||
"x.com.samsung.da.progress": "0",
|
||||
"x.com.samsung.da.settingStatus": "Off",
|
||||
"x.com.samsung.da.supportedSettingStatus": [
|
||||
"On",
|
||||
"Off"
|
||||
],
|
||||
"defaultSettingStatus": "On"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/muteonce/vs/0",
|
||||
"rep": {
|
||||
"muteonce": "Off",
|
||||
"rt": [
|
||||
"x.com.samsung.da.option.muteonce"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/personality/presence/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "Off",
|
||||
"operationNumber": "2",
|
||||
"displayCondition": "Enable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.longnotisubscription": "false",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/reserverulesets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "8EFFFFFFFF101E121EFFFFFFFFFFFF000001001F0001000000000000009C00FFFF1E00",
|
||||
"x.com.samsung.da.id": "RAC",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/selfcheck/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.start": "Cancel",
|
||||
"x.com.samsung.da.status": "Ready",
|
||||
"x.com.samsung.da.progress": "0",
|
||||
"x.com.samsung.da.result": "NA",
|
||||
"x.com.samsung.da.supportedActions": [
|
||||
"Start",
|
||||
"Cancel"
|
||||
],
|
||||
"x.com.samsung.da.error": [
|
||||
"DA_ERROR_NONE"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/control/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.increment": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Temperature",
|
||||
"x.com.samsung.da.current": "29.0",
|
||||
"x.com.samsung.da.desired": "18.0",
|
||||
"x.com.samsung.da.minimum": "16",
|
||||
"x.com.samsung.da.maximum": "30",
|
||||
"x.com.samsung.da.increment": "1",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/oscillation/vs/0",
|
||||
"rep": {
|
||||
"vertical": "Fix",
|
||||
"supportedVertical": [
|
||||
"Swing",
|
||||
"Fix"
|
||||
],
|
||||
"verticalAngle": "4",
|
||||
"supportedVerticalAngle": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6"
|
||||
],
|
||||
"horizontal": "Fix",
|
||||
"supportedHorizontal": [
|
||||
"Swing",
|
||||
"Fix"
|
||||
],
|
||||
"horizontalSwingMode": "AllSwing",
|
||||
"supportedHorizontalSwingMode": [
|
||||
"AllSwing",
|
||||
"LeftSwing",
|
||||
"RightSwing"
|
||||
],
|
||||
"horizontalAngle": "3",
|
||||
"supportedHorizontalAngle": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/strength/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "1",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4"
|
||||
],
|
||||
"x.com.samsung.da.modesName": [
|
||||
"Auto",
|
||||
"Low",
|
||||
"Mid",
|
||||
"High",
|
||||
"Turbo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "0",
|
||||
"otnCompleteDate": "noHistory",
|
||||
"scheduledTime": "None",
|
||||
"swVersionInfo": {
|
||||
"platform": "Tizen Lite",
|
||||
"oneUiVersion": "7.0 Air conditioner",
|
||||
"osVersion": "4.0"
|
||||
},
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "ARA-WW-TP1-26-AR6X00MG",
|
||||
"versions": [
|
||||
"11260401"
|
||||
],
|
||||
"visVersion": "260401"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "845241131741FFFFFFFF",
|
||||
"versions": [
|
||||
"25102900",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "251029"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "045210281241FFFFFFFF",
|
||||
"versions": [
|
||||
"25111800",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "251118"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "045210281341FFFFFFFF",
|
||||
"versions": [
|
||||
"25101500",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "251015"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "0452FFFFFFFFFFFFFFFF",
|
||||
"versions": [
|
||||
"FFFFFFFF",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "999999"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Kuala_Lumpur",
|
||||
"offset": "+08:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connectionconfig/vs/0",
|
||||
"rep": {
|
||||
"autoReconnectionMinVersion": "1.0",
|
||||
"autoReconnection": "true",
|
||||
"autoReconnectionProtocolType": [
|
||||
"helper_hotspot",
|
||||
"ble_ocf"
|
||||
],
|
||||
"supportedWiFiAuthType": [
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA-PSK",
|
||||
"WPA2-PSK",
|
||||
"SAE"
|
||||
],
|
||||
"supportedWiFiCryptoType": [
|
||||
"TKIP",
|
||||
"AES",
|
||||
"WEP-64",
|
||||
"WEP-128"
|
||||
],
|
||||
"supportedWiFiFreq": [
|
||||
"2.4G"
|
||||
],
|
||||
"calmConnectionCare": {
|
||||
"version": "1.0",
|
||||
"role": [
|
||||
"things"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wirelessinfo/vs/0",
|
||||
"rep": {
|
||||
"macaddressWiFi": "**REDACTED**",
|
||||
"macaddressBLE": "**REDACTED**",
|
||||
"connectedApSsid": "K9 Unit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/quickcontrol/info/vs/0",
|
||||
"rep": {
|
||||
"supportedVersion": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/dginformation/vs/0",
|
||||
"rep": {
|
||||
"enrolmentstatus": "Unknown",
|
||||
"devicestate": "Unknown",
|
||||
"lockstatus": "Unknown",
|
||||
"nextduedate": "",
|
||||
"workingminutes": 0,
|
||||
"paymentinfo": {
|
||||
"emiplan": "Unknown",
|
||||
"currency": "Unknown",
|
||||
"totalemi": 0,
|
||||
"totalemipaid": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/0",
|
||||
"rep": {
|
||||
"temperature": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+523
@@ -0,0 +1,523 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/aisleep/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.displayNightMode": "Off",
|
||||
"x.com.samsung.da.elapsedTime": "0",
|
||||
"x.com.samsung.da.requestFeedback": "Off",
|
||||
"x.com.samsung.da.resultFeedback": "0",
|
||||
"x.com.samsung.da.statusFeedback": "Idle",
|
||||
"x.com.samsung.da.sleepTime": "14002200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-26T06:26:40",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "FilterAlarm_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-26T06:26:40",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/availablecontrolsets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "8D0D00B4012C3F21064804000000",
|
||||
"x.com.samsung.da.id": "RAC",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "2647000000",
|
||||
"x.com.samsung.da.airconOptionList": [
|
||||
"SingleCommand_1",
|
||||
"DR",
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"AI_RAC_KOREA_COOLONLY_3.0",
|
||||
"AI_3.0",
|
||||
"Auto_To_AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connectionconfig/vs/0",
|
||||
"rep": {
|
||||
"autoReconnectionMinVersion": "1.0",
|
||||
"autoReconnection": "true",
|
||||
"autoReconnectionProtocolType": [
|
||||
"helper_hotspot",
|
||||
"ble_ocf"
|
||||
],
|
||||
"supportedWiFiAuthType": [
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA-PSK",
|
||||
"WPA2-PSK",
|
||||
"SAE"
|
||||
],
|
||||
"supportedWiFiCryptoType": [
|
||||
"TKIP",
|
||||
"AES",
|
||||
"WEP-64",
|
||||
"WEP-128"
|
||||
],
|
||||
"supportedWiFiFreq": [
|
||||
"2.4G"
|
||||
],
|
||||
"calmConnectionCare": {
|
||||
"version": "1.0",
|
||||
"role": [
|
||||
"things"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/0",
|
||||
"rep": {
|
||||
"DRLevel": 0,
|
||||
"start": "1970-01-01T00:00:00Z",
|
||||
"duration": 0,
|
||||
"override": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.drlcLevel": "0",
|
||||
"x.com.samsung.da.duration": "00:00:00",
|
||||
"x.com.samsung.da.drlcStartTime": "1970-01-01T00:00:00Z",
|
||||
"x.com.samsung.da.override": "Off",
|
||||
"x.com.samsung.da.realSaving": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {
|
||||
"power": 244.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "244.000000",
|
||||
"x.com.samsung.da.cumulativePower": "11001",
|
||||
"x.com.samsung.da.cumulativeSavedPower": "0",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePowerType": "total"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+09:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/airdustfilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsage": "20",
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterDesiredUsage": "500",
|
||||
"x.com.samsung.da.filterStatus": "normal",
|
||||
"x.com.samsung.da.filterCapacity": "500",
|
||||
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"replaceable",
|
||||
"washable"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/0",
|
||||
"rep": {
|
||||
"humidity": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.humidity": "0",
|
||||
"x.com.samsung.da.fivepercentHumidity": "60"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "TP1X_DA_AC_WAC_01001_0000|10254341|60050000001711014E00402000002000",
|
||||
"x.com.samsung.da.description": "TP1X_DA_AC_WAC_01001_0000",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "AW0",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagTsId": "DA01",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02545A260601",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "02543A24061800,FFFFFFFFFFFFFF",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Outdoor",
|
||||
"x.com.samsung.da.number": "02580A10000100,FFFFFFFFFFFFFF"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "3",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Touch IC",
|
||||
"x.com.samsung.da.number": "02553A23031600,FFFFFFFFFFFFFF"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/keepnormalstate/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.keepnormal": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/light/vs/0",
|
||||
"rep": {
|
||||
"mode": "On",
|
||||
"supportedModes": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/convenient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Off",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Off",
|
||||
"Quiet",
|
||||
"Nano",
|
||||
"NanoSleep"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"AIComfort",
|
||||
"Cool",
|
||||
"Dry",
|
||||
"Fan"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"Cool"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"OnTimer_0",
|
||||
"OffTimer_0",
|
||||
"ArtificialWorking_Off",
|
||||
"ComfortAICooling_Off",
|
||||
"AiTempChanged_Off",
|
||||
"AiTemp_230",
|
||||
"Volume_Mute",
|
||||
"OutdoorTemp_97",
|
||||
"CoolCapa_0",
|
||||
"StopAutoClean_Idle",
|
||||
"Autoclean_Off",
|
||||
"OptionCode_51320",
|
||||
"ExtendOptionCode_230093",
|
||||
"RacInfo_None",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"DurationOn_93",
|
||||
"WelcomeCoolingState_Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/autoclean/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Stop",
|
||||
"x.com.samsung.da.settingStatus": "Off",
|
||||
"x.com.samsung.da.progress": "0",
|
||||
"x.com.samsung.da.supportedStatus": [
|
||||
"Start",
|
||||
"Stop"
|
||||
],
|
||||
"x.com.samsung.da.supportedSettingStatus": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/muteonce/vs/0",
|
||||
"rep": {
|
||||
"muteonce": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"x.com.samsung.da.newVersionNo": "00000000",
|
||||
"x.com.samsung.da.currentVersionInfo": "00000000",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "",
|
||||
"otnTarget": "main",
|
||||
"otnCompleteDate": "noHistory",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "AWA-KR-TP1-23-AW7000",
|
||||
"versions": [
|
||||
"15260601"
|
||||
],
|
||||
"visVersion": "260601"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "042010254341FFFFFFFF",
|
||||
"versions": [
|
||||
"24061800",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "240618"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "042010258041FFFFFFFF",
|
||||
"versions": [
|
||||
"10000100",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "100001"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "042010255341FFFFFFFF",
|
||||
"versions": [
|
||||
"23031600",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "230316"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/personality/presence/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "",
|
||||
"x.com.samsung.da.deviceId": "**REDACTED**",
|
||||
"x.com.samsung.da.value": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "On",
|
||||
"causeSource": "SMTS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/quickcontrol/info/vs/0",
|
||||
"rep": {
|
||||
"supportedVersion": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.longnotisubscription": "true",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/reserverulesets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "8DFFFFFFFF121E121E0000FFFFFFFFFFFF01000000011F0000000000000000001F8C00",
|
||||
"x.com.samsung.da.id": "WAC",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/selfcheck/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.start": "Cancel",
|
||||
"x.com.samsung.da.status": "Ready",
|
||||
"x.com.samsung.da.progress": "0",
|
||||
"x.com.samsung.da.result": "NA",
|
||||
"x.com.samsung.da.supportedActions": [
|
||||
"Start",
|
||||
"Cancel"
|
||||
],
|
||||
"x.com.samsung.da.error": [
|
||||
"DA_ERROR_NONE"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sensors/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/control/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.increment": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/current/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
16,
|
||||
30
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 30.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
16,
|
||||
30
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 30.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Temperature",
|
||||
"x.com.samsung.da.desired": "30.0",
|
||||
"x.com.samsung.da.current": "30.0",
|
||||
"x.com.samsung.da.maximum": "30",
|
||||
"x.com.samsung.da.minimum": "18",
|
||||
"x.com.samsung.da.increment": "1.0",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Seoul",
|
||||
"offset": "+09:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/welcome/temperature/vs/0",
|
||||
"rep": {
|
||||
"operatingStatus": "None",
|
||||
"requestId": "0000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/direction/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Fix",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Fix",
|
||||
"Left_And_Right"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/strength/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "1",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
],
|
||||
"x.com.samsung.da.modesName": [
|
||||
"Auto",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wirelessinfo/vs/0",
|
||||
"rep": {
|
||||
"macaddressWiFi": "**REDACTED**",
|
||||
"macaddressBLE": "**REDACTED**",
|
||||
"connectedApSsid": "**REDACTED**"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+386
@@ -0,0 +1,386 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/personality/presence/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "",
|
||||
"x.com.samsung.da.deviceId": "**REDACTED**",
|
||||
"x.com.samsung.da.value": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.longnotisubscription": "false",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/airdustfilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsage": "100",
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterDesiredUsage": "224",
|
||||
"x.com.samsung.da.filterStatus": "wash",
|
||||
"x.com.samsung.da.filterCapacity": "224",
|
||||
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"washable"
|
||||
],
|
||||
"x.com.samsung.da.supportedFilterDesiredUsage": [
|
||||
"224",
|
||||
"300",
|
||||
"500",
|
||||
"700"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/convenient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Off",
|
||||
"Sleep"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/autoclean/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Stop",
|
||||
"x.com.samsung.da.settingStatus": "On",
|
||||
"x.com.samsung.da.progress": "100",
|
||||
"cleaningTime": "30",
|
||||
"minCleaningTime": "10",
|
||||
"maxCleaningTime": "60",
|
||||
"cleaningTimeUnit": "10",
|
||||
"x.com.samsung.da.supportedStatus": [
|
||||
"Start",
|
||||
"TimedClean",
|
||||
"Stop"
|
||||
],
|
||||
"x.com.samsung.da.supportedSettingStatus": [
|
||||
"On",
|
||||
"TimedClean",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-26T10:35:53",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "WaterTankFull_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-26T10:35:53",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "259.000000",
|
||||
"x.com.samsung.da.cumulativePower": "183099",
|
||||
"x.com.samsung.da.cumulativeSavedPower": "0",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePowerType": "total"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Smart",
|
||||
"Max",
|
||||
"High",
|
||||
"Medium",
|
||||
"Quiet",
|
||||
"ClothesDrying"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"Smart"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"Volume_Mute",
|
||||
"RacInfo_None",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"WelcomeCoolingState_Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "On",
|
||||
"causeSource": "SMTS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sensors/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "TP1X_DA_AC_DHM_01001_0000|10253841|77000000001700000A00000000000000",
|
||||
"x.com.samsung.da.description": "TP1X_DA_AC_DHM_01001_0000",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "AY0",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagTsId": "DA01",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02544A260401",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "02538A24032700,FFFFFFFFFFFFFF",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Outdoor",
|
||||
"x.com.samsung.da.number": "02590A10000100,FFFFFFFFFFFFFF"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "4",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Touch IC",
|
||||
"x.com.samsung.da.number": "02552A23020200,FFFFFFFFFFFFFF"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+09:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "0000000000",
|
||||
"x.com.samsung.da.airconOptionList": [
|
||||
"SingleCommand_1",
|
||||
"DR",
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"PRODUCT_GLOBAL",
|
||||
"AI_RAC_GLOBAL_2.0",
|
||||
"AI_Heat"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.humidity": "47",
|
||||
"x.com.samsung.da.desiredHumidity": "50",
|
||||
"increment": "5",
|
||||
"x.com.samsung.da.fivepercentHumidity": "46"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.drlcLevel": "0",
|
||||
"x.com.samsung.da.durationminutes": "0",
|
||||
"x.com.samsung.da.start": "1970-01-01T00:00:00Z",
|
||||
"x.com.samsung.da.override": "Off",
|
||||
"x.com.samsung.da.realSaving": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/availablecontrolsets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "20001E46070A5A00000000000000",
|
||||
"x.com.samsung.da.id": "DHM",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/da/softreset/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.softwarereset": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/keepnormalstate/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.keepnormal": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"x.com.samsung.da.newVersionNo": "00000000",
|
||||
"x.com.samsung.da.currentVersionInfo": "00000000",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "",
|
||||
"otnTarget": "main",
|
||||
"otnCompleteDate": "noHistory",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "ADH-WW-TP1-23-AY7100",
|
||||
"versions": [
|
||||
"18260401"
|
||||
],
|
||||
"visVersion": "260401"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "042110253841FFFFFFFF",
|
||||
"versions": [
|
||||
"24032700",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "240327"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "042110259041FFFFFFFF",
|
||||
"versions": [
|
||||
"10000100",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "100001"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "042110255241FFFFFFFF",
|
||||
"versions": [
|
||||
"23020200",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "230202"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Seoul",
|
||||
"offset": "+09:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/muteonce/vs/0",
|
||||
"rep": {
|
||||
"muteonce": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/reserverulesets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "3F1E461E461E46FFFFFFFF1E4600",
|
||||
"x.com.samsung.da.id": "DHM",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connectionconfig/vs/0",
|
||||
"rep": {
|
||||
"autoReconnectionMinVersion": "1.0",
|
||||
"autoReconnection": "true",
|
||||
"autoReconnectionProtocolType": [
|
||||
"helper_hotspot",
|
||||
"ble_ocf"
|
||||
],
|
||||
"supportedWiFiAuthType": [
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA-PSK",
|
||||
"WPA2-PSK",
|
||||
"SAE"
|
||||
],
|
||||
"supportedWiFiCryptoType": [
|
||||
"TKIP",
|
||||
"AES",
|
||||
"WEP-64",
|
||||
"WEP-128"
|
||||
],
|
||||
"supportedWiFiFreq": [
|
||||
"2.4G"
|
||||
],
|
||||
"calmConnectionCare": {
|
||||
"version": "1.0",
|
||||
"role": [
|
||||
"things"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/welcome/humidity/vs/0",
|
||||
"rep": {
|
||||
"requestId": "0000",
|
||||
"operatingStatus": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wirelessinfo/vs/0",
|
||||
"rep": {
|
||||
"macaddressWiFi": "**REDACTED**",
|
||||
"macaddressBLE": "**REDACTED**"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/quickcontrol/info/vs/0",
|
||||
"rep": {
|
||||
"supportedVersion": "1.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "0000000000",
|
||||
"x.com.samsung.da.countryCode": "US"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connectionconfig/vs/0",
|
||||
"rep": {
|
||||
"autoReconnectionMinVersion": "1.0",
|
||||
"autoReconnection": "true",
|
||||
"autoReconnectionProtocolType": [
|
||||
"helper_hotspot",
|
||||
"ble_ocf"
|
||||
],
|
||||
"supportedWiFiAuthType": [
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA-PSK",
|
||||
"WPA2-PSK",
|
||||
"SAE"
|
||||
],
|
||||
"supportedWiFiCryptoType": [
|
||||
"TKIP",
|
||||
"AES",
|
||||
"WEP-64",
|
||||
"WEP-128"
|
||||
],
|
||||
"supportedWiFiFreq": [
|
||||
"2.4G"
|
||||
],
|
||||
"calmConnectionCare": {
|
||||
"version": "1.0",
|
||||
"role": [
|
||||
"things"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/course/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"HOMECARE_WIZARD_V2"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"DeviceType_0152",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"Course_01",
|
||||
"AiOption_On",
|
||||
"MixedLoadBell_Disable",
|
||||
"MixedLoadBellNoti_Nothing",
|
||||
"LaundryOutTime_0",
|
||||
"SeamlessControl_Enable",
|
||||
"KidsLockBypass_On",
|
||||
"DetergentOnce_1",
|
||||
"DetergentLeft_0",
|
||||
"DetergentBase_5",
|
||||
"DetergentAlarm_Off",
|
||||
"DetergentType_3",
|
||||
"DetergentTotal_0",
|
||||
"SpecialFunction_20",
|
||||
"AvailableDelayTime_39",
|
||||
"LaundryPlannerUserSetTime_0",
|
||||
"ProgressTimeSet_B20870B400B4",
|
||||
"WrinklePreventRunning_Off",
|
||||
"DryTime_0",
|
||||
"SendToDevice_Off",
|
||||
"GMT_F6",
|
||||
"DryTimeSet_0A00000000500A005A00000000000000001E0A005A00000000000000001414005A000000000000000000000000000000000000000000000000000000000000000000000000140A005A280A005A",
|
||||
"EnergyLevelSet_0504040504040205020204040305020203030103",
|
||||
"MostUsed_068A3ED000",
|
||||
"WrinklePreventSet_0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F",
|
||||
"MixedLoadBellSet_02FF02FFFF02FFFFFF0202FF02FFFF02FFFFFF",
|
||||
"GeoFenceAlarm",
|
||||
"UsagesDB_ok",
|
||||
"EnergyKW_396",
|
||||
"DrumCleanLog_Empty",
|
||||
"TimeSync_NotSupported"
|
||||
],
|
||||
"x.com.samsung.da.supportedOptions": [
|
||||
"2018410D33E06833ED0002F8520D33E178520D3083E8520D000078102D33E338520D520358520D000348520D0000E8520D33E058520D33E308410D308318410D33E098410D3080F8204D308328308D33E2E8102D308368000D0000D8102D000"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cycleinterface/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cycleInterfaceEnabled": "On"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/diagnosis/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "-500",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePower": "2323800",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.cumulativeDate": "1784991600",
|
||||
"x.com.samsung.da.cumulativeDateUTC": "1785009600"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "-05:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "DA_WM_TP1_21_COMMON|20286441|300000010015110002A3031700000000",
|
||||
"x.com.samsung.da.description": "DA_WM_TP1_21_COMMON_DVE50A8800_8600/DC92-02835A_0080",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "310",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagTsId": "DA01",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "DA_WM_TP1_21_COMMON|20286441|300000010015110002A3031700000000",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02986A260118(A182)",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Firmware_1_DB_20286441210514150FFFFF202835412105131904FFFF(01522028644120283541_30000000)(FileDown:0)(Type:0)",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "02864A21051415,02835A21051319",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.kidsLock": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/0",
|
||||
"rep": {
|
||||
"currentMachineState": "**REDACTED**",
|
||||
"machineStates": "**REDACTED**",
|
||||
"jobStates": [
|
||||
"None",
|
||||
"Drying",
|
||||
"Cooling",
|
||||
"Finish"
|
||||
],
|
||||
"currentJobState": "None",
|
||||
"remainingTime": "01:15:00",
|
||||
"progressPercentage": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.state": "Ready",
|
||||
"x.com.samsung.da.remainingTime": "01:15:00",
|
||||
"x.com.samsung.da.progressPercentage": "1",
|
||||
"x.com.samsung.da.progress": "None",
|
||||
"x.com.samsung.da.delayEndTime": "00:00:00",
|
||||
"x.com.samsung.da.supportedProgress": [
|
||||
"None",
|
||||
"Drying",
|
||||
"Cooling",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"x.com.samsung.da.newVersionNo": "00000000",
|
||||
"x.com.samsung.da.currentVersionInfo": "00000000",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "",
|
||||
"otnTarget": "main",
|
||||
"otnCompleteDate": "2026-03-04",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "DA_WM_TP1_21_COMMON",
|
||||
"versions": [
|
||||
"30260118"
|
||||
],
|
||||
"visVersion": "260118"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "01522028644120283541",
|
||||
"versions": [
|
||||
"21051415",
|
||||
"21051319"
|
||||
],
|
||||
"visVersion": "210514"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/quickcontrol/info/vs/0",
|
||||
"rep": {
|
||||
"supportedVersion": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "10",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.remoteControlEnabled": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/setting/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedSetLanguage": [
|
||||
"en_US",
|
||||
"ko_KR",
|
||||
"fr_CA",
|
||||
"es_MX",
|
||||
"pt_BR"
|
||||
],
|
||||
"x.com.samsung.da.setLanguage": "en_US"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/st/dryercourse/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.st.dryerMode": "Table_03_Course_01",
|
||||
"x.com.samsung.da.st.courseTable": "Table_03"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "America/Chicago",
|
||||
"offset": "-05:00",
|
||||
"DST": "ON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/washer/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.wrinklePrevent": "Off",
|
||||
"x.com.samsung.da.waterTemperature": "Medium",
|
||||
"x.com.samsung.da.supportedWaterTemperature": [
|
||||
"None",
|
||||
"ExtraLow",
|
||||
"Low",
|
||||
"MediumLow",
|
||||
"Medium",
|
||||
"High"
|
||||
],
|
||||
"x.com.samsung.da.dryLevel": "Normal",
|
||||
"x.com.samsung.da.supportedDryLevel": [
|
||||
"None",
|
||||
"Damp",
|
||||
"Less",
|
||||
"Normal",
|
||||
"More",
|
||||
"Very"
|
||||
],
|
||||
"x.com.samsung.da.dryerType": "Electricity"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wirelessinfo/vs/0",
|
||||
"rep": {
|
||||
"macaddressWiFi": "**REDACTED**",
|
||||
"macaddressBLE": "**REDACTED**"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/editcourse/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.editCourseList": "EditCourseList_0106",
|
||||
"x.com.samsung.da.fixedCourseList": "FixedCourseList_0101"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/jobbeginingstatus/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.currentStatus": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/personalcourse/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.courses": [
|
||||
"F1_00",
|
||||
"F2_00",
|
||||
"F3_00",
|
||||
"F4_00",
|
||||
"F5_00",
|
||||
"F6_00",
|
||||
"F7_00",
|
||||
"F8_00",
|
||||
"F9_00",
|
||||
"FA_00"
|
||||
],
|
||||
"x.com.samsung.da.maxCourseNum": "10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/setinfo/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.isModelSettingWithoutSC": "true",
|
||||
"x.com.samsung.da.aiCourse": "false",
|
||||
"x.com.samsung.da.isModelSettingPowerOnOff": "false",
|
||||
"x.com.samsung.da.modelCode": "M(None),W(DVE50A8600V/A3)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/welcomemsg/vs/0",
|
||||
"rep": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"child_lock",
|
||||
"clean_level",
|
||||
"device_active",
|
||||
"display",
|
||||
"display_light",
|
||||
"dust",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"fan",
|
||||
"fine_dust",
|
||||
"firmware_update",
|
||||
"hepa_filter_status",
|
||||
"hepa_filter_usage",
|
||||
"mute_once",
|
||||
"odor",
|
||||
"panel_status",
|
||||
"pet_filter_activation",
|
||||
"power_switch",
|
||||
"sound_mode",
|
||||
"sound_output",
|
||||
"sound_volume",
|
||||
"super_fine_dust"
|
||||
]
|
||||
}
|
||||
+3
@@ -6,9 +6,12 @@
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"diagnosis_status",
|
||||
"display_light",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"humidity",
|
||||
"power_watts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"power_watts"
|
||||
]
|
||||
}
|
||||
@@ -5,10 +5,13 @@
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"diagnosis_status",
|
||||
"display_light",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"power_watts"
|
||||
]
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"selfcheck_error",
|
||||
"selfcheck_result",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"climate",
|
||||
"current_limit_enabled",
|
||||
"current_limit_level",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"power_watts"
|
||||
]
|
||||
}
|
||||
@@ -5,8 +5,11 @@
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
"energy_kwh",
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"power_watts"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"air_purify",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"diagnosis_status",
|
||||
"display_light",
|
||||
"energy_kwh",
|
||||
"humidity",
|
||||
"power_watts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"overload_protection_active",
|
||||
"overload_protection_mode",
|
||||
"selfcheck_error",
|
||||
"selfcheck_result",
|
||||
"selfcheck_status"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"power_watts",
|
||||
"selfcheck_error",
|
||||
"selfcheck_result",
|
||||
"selfcheck_status"
|
||||
]
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"operating_mode",
|
||||
"power_switch",
|
||||
"power_watts",
|
||||
"target_humidity"
|
||||
]
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"child_lock",
|
||||
"completion_minutes",
|
||||
"cycle",
|
||||
"cycle_active",
|
||||
"delay_start_hours",
|
||||
"diagnosis",
|
||||
"dry_level",
|
||||
"dry_time",
|
||||
"dryer_type",
|
||||
"energy_kwh",
|
||||
"finish_time",
|
||||
"firmware_update",
|
||||
"job_beginning_status",
|
||||
"machine_state",
|
||||
"power_switch",
|
||||
"progress",
|
||||
"progress_percentage",
|
||||
"remote_control",
|
||||
"wrinkle_prevent"
|
||||
]
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"burner_0_hot_surface",
|
||||
"burner_0_pan_detected",
|
||||
"burner_0_power_level",
|
||||
"burner_0_state",
|
||||
"burner_1_hot_surface",
|
||||
"burner_1_pan_detected",
|
||||
"burner_1_power_level",
|
||||
"burner_1_state",
|
||||
"burner_2_hot_surface",
|
||||
"burner_2_pan_detected",
|
||||
"burner_2_power_level",
|
||||
"burner_2_state",
|
||||
"cooktop_child_lock",
|
||||
"cooktop_power",
|
||||
"cooktop_state",
|
||||
"energy_kwh",
|
||||
"firmware_update",
|
||||
"paired_hood_connected",
|
||||
"paired_hood_fan_speed",
|
||||
"paired_hood_firmware",
|
||||
"paired_hood_light",
|
||||
"paired_hood_model",
|
||||
"paired_hood_power",
|
||||
"probe_battery",
|
||||
"probe_connected",
|
||||
"probe_target_temperature",
|
||||
"probe_temperature"
|
||||
]
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"child_lock",
|
||||
"cloud_connected",
|
||||
"cook_time",
|
||||
"current_temp_c",
|
||||
"cycle_active",
|
||||
"door_open",
|
||||
"energy_kwh",
|
||||
"fast_preheat",
|
||||
"finish_time",
|
||||
"firmware_update",
|
||||
"lamp",
|
||||
"machine_state",
|
||||
"natural_steam",
|
||||
"operation_time_minutes",
|
||||
"oven_mode",
|
||||
"oven_setpoint",
|
||||
"oven_state",
|
||||
"progress_percentage",
|
||||
"remote_control",
|
||||
"sound"
|
||||
]
|
||||
}
|
||||
Vendored
+6
@@ -2,20 +2,26 @@
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"burner_0_hot_surface",
|
||||
"burner_0_pan_detected",
|
||||
"burner_0_power_level",
|
||||
"burner_0_state",
|
||||
"burner_1_hot_surface",
|
||||
"burner_1_pan_detected",
|
||||
"burner_1_power_level",
|
||||
"burner_1_state",
|
||||
"burner_2_hot_surface",
|
||||
"burner_2_pan_detected",
|
||||
"burner_2_power_level",
|
||||
"burner_2_state",
|
||||
"burner_3_hot_surface",
|
||||
"burner_3_pan_detected",
|
||||
"burner_3_power_level",
|
||||
"burner_3_state",
|
||||
"child_lock",
|
||||
"cloud_connected",
|
||||
"cook_time",
|
||||
"cooktop_child_lock",
|
||||
"cooktop_power",
|
||||
"cooktop_safety_shutoff_enabled",
|
||||
"cooktop_state",
|
||||
"current_temp_c",
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"child_lock",
|
||||
"cloud_connected",
|
||||
"cook_time",
|
||||
"cooktop_running_state",
|
||||
"current_temp_c",
|
||||
"cycle_active",
|
||||
"door_open",
|
||||
"fast_preheat",
|
||||
"finish_time",
|
||||
"firmware_update",
|
||||
"lamp",
|
||||
"machine_state",
|
||||
"natural_steam",
|
||||
"operation_time_minutes",
|
||||
"oven_mode",
|
||||
"oven_setpoint",
|
||||
"oven_state",
|
||||
"power_switch",
|
||||
"progress_percentage",
|
||||
"remote_control",
|
||||
"sound",
|
||||
"warming_center_state"
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"child_lock",
|
||||
"cloud_connected",
|
||||
"cook_time",
|
||||
"cooktop_running_state",
|
||||
"current_temp_c",
|
||||
"cycle_active",
|
||||
"door_open",
|
||||
"fast_preheat",
|
||||
"finish_time",
|
||||
"firmware_update",
|
||||
"lamp",
|
||||
"machine_state",
|
||||
"natural_steam",
|
||||
"operation_time_minutes",
|
||||
"oven_mode",
|
||||
"oven_setpoint",
|
||||
"oven_state",
|
||||
"power_switch",
|
||||
"progress_percentage",
|
||||
"remote_control",
|
||||
"sound",
|
||||
"warming_center_state"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"defrost_active",
|
||||
"defrost_delay",
|
||||
"diagnosis_status",
|
||||
"door_onedoorfreezer_open",
|
||||
"energy_kwh",
|
||||
"energy_last_month_kwh",
|
||||
"energy_saved_kwh",
|
||||
"energy_this_month_kwh",
|
||||
"firmware_update",
|
||||
"freezer_setpoint",
|
||||
"freezer_temperature",
|
||||
"ice_maker_enabled",
|
||||
"power_energy_kwh",
|
||||
"power_watts",
|
||||
"rapid_freezing",
|
||||
"rapid_fridge",
|
||||
"sabbath_mode"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"cooler_setpoint",
|
||||
"cooler_temperature",
|
||||
"defrost_active",
|
||||
"defrost_delay",
|
||||
"diagnosis_status",
|
||||
"door_cooler_open",
|
||||
"door_onedoorfreezer_open",
|
||||
"energy_kwh",
|
||||
"energy_last_month_kwh",
|
||||
"energy_saved_kwh",
|
||||
"energy_this_month_kwh",
|
||||
"firmware_update",
|
||||
"ice_maker_enabled",
|
||||
"power_energy_kwh",
|
||||
"power_watts",
|
||||
"rapid_freezing",
|
||||
"rapid_fridge",
|
||||
"sabbath_mode"
|
||||
]
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"auto_empty",
|
||||
"cleanstation_status",
|
||||
"discharging_time",
|
||||
"dustbag_full",
|
||||
"dustbag_usage",
|
||||
"dustbin_auto_close",
|
||||
"energy_kwh",
|
||||
"firmware_update",
|
||||
"stick_status",
|
||||
"uvc_emitted_time",
|
||||
"uvc_finished_time",
|
||||
"uvc_intensive_mode",
|
||||
"uvc_operation_time",
|
||||
"uvc_total_operation_time"
|
||||
]
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"child_lock",
|
||||
"completion_minutes",
|
||||
"cycle",
|
||||
"cycle_active",
|
||||
"delay_start_hours",
|
||||
"detergent_low",
|
||||
"detergent_quantity",
|
||||
"detergent_water_hardness",
|
||||
"diagnosis_status",
|
||||
"drum_clean_cycles_remaining",
|
||||
"energy_kwh",
|
||||
"finish_time",
|
||||
"firmware_update",
|
||||
"job_beginning_status",
|
||||
"machine_state",
|
||||
"power_switch",
|
||||
"progress",
|
||||
"progress_percentage",
|
||||
"remote_control",
|
||||
"rinse_cycles",
|
||||
"softener_concentration",
|
||||
"softener_low",
|
||||
"softener_quantity",
|
||||
"spin_speed",
|
||||
"wash_temperature"
|
||||
]
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"child_lock",
|
||||
"completion_minutes",
|
||||
"cycle",
|
||||
"cycle_active",
|
||||
"delay_start_hours",
|
||||
"detergent_low",
|
||||
"diagnosis_status",
|
||||
"drum_clean_cycles_remaining",
|
||||
"energy_kwh",
|
||||
"finish_time",
|
||||
"firmware_update",
|
||||
"job_beginning_status",
|
||||
"machine_state",
|
||||
"power_switch",
|
||||
"progress",
|
||||
"progress_percentage",
|
||||
"remote_control",
|
||||
"rinse_cycles",
|
||||
"softener_low",
|
||||
"spin_speed",
|
||||
"wash_temperature"
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"buzz_lock",
|
||||
"coldwater_lock",
|
||||
"dispense_capacity",
|
||||
"dispense_type",
|
||||
"favorite_capacity",
|
||||
"favorite_capacity_enabled",
|
||||
"filter_clean_remain_time",
|
||||
"filter_door_status",
|
||||
"filter_status",
|
||||
"filter_usage",
|
||||
"firmware_update",
|
||||
"hot_water_temperature",
|
||||
"hotwater_lock",
|
||||
"pouring",
|
||||
"selfcheck_error",
|
||||
"selfcheck_result",
|
||||
"selfcheck_status",
|
||||
"sterilize_last_time",
|
||||
"sterilize_period",
|
||||
"sterilize_plan_time",
|
||||
"sterilize_run_time",
|
||||
"waterpurifier_status"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"buzz_lock",
|
||||
"coffee_brew_status",
|
||||
"coldwater_lock",
|
||||
"dispense_capacity",
|
||||
"dispense_type",
|
||||
"favorite_capacity",
|
||||
"favorite_capacity_enabled",
|
||||
"favorite_coffee_enabled",
|
||||
"favorite_hotwater_enabled",
|
||||
"favorite_hotwater_temperature",
|
||||
"filter_clean_remain_time",
|
||||
"filter_door_status",
|
||||
"filter_status",
|
||||
"filter_usage",
|
||||
"firmware_update",
|
||||
"hot_water_temperature",
|
||||
"hotwater_lock",
|
||||
"pouring",
|
||||
"selfcheck_error",
|
||||
"selfcheck_result",
|
||||
"selfcheck_status",
|
||||
"sterilize_last_time",
|
||||
"sterilize_period",
|
||||
"sterilize_plan_time",
|
||||
"sterilize_run_time",
|
||||
"waterpurifier_status"
|
||||
]
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"rt": [
|
||||
"x.com.samsung.da.alarms"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.s"
|
||||
],
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "CT_E_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-26T06:30:18"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/bluetooth/hood/status/vs/0",
|
||||
"rep": {
|
||||
"connectionState": "disconnected",
|
||||
"micomModelId": "",
|
||||
"firmwareVersion": "",
|
||||
"power": "off",
|
||||
"fanSpeed": 0,
|
||||
"lampState": "off",
|
||||
"timer": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/bluetooth/probe/status/vs/0",
|
||||
"rep": {
|
||||
"connectionState": "disconnected",
|
||||
"batteryState": "normal",
|
||||
"batteryPercentage": 0,
|
||||
"operationBurnerNumber": -1,
|
||||
"temperatureUnit": "C",
|
||||
"targetTemperature": 0,
|
||||
"currentTemperature": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cooktop/recipe/status/vs/0",
|
||||
"rep": {
|
||||
"sequenceNumber": 160,
|
||||
"operationBurnerNumber": 2,
|
||||
"stageInfo": {
|
||||
"numberOfStages": 0,
|
||||
"cookingTimeList": []
|
||||
},
|
||||
"textData": {
|
||||
"menu": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cooktop/spec/vs/0",
|
||||
"rep": {
|
||||
"deviceType": "cooktop",
|
||||
"micomModelId": "NV8500T-/KO4",
|
||||
"supportedFeatureList": [
|
||||
"kitchenService"
|
||||
],
|
||||
"numberOfBurners": 3,
|
||||
"supportedBurnerList": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"supportedPowerLevelList": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"boost",
|
||||
"0",
|
||||
"keepWarm"
|
||||
],
|
||||
"supportedRecipeProtocolVersion": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cooktop/status/vs/0",
|
||||
"rep": {
|
||||
"sequenceNumber": 3214,
|
||||
"power": "off",
|
||||
"childLock": "off",
|
||||
"operationState": "ready",
|
||||
"additionalStateList": [
|
||||
""
|
||||
],
|
||||
"burnerList": [
|
||||
{
|
||||
"burnerNumber": 0,
|
||||
"mode": "normal",
|
||||
"operationState": "ready",
|
||||
"powerLevel": "0",
|
||||
"hotSurfaceState": "normal",
|
||||
"panDetection": true,
|
||||
"timer": {
|
||||
"operationState": "ready",
|
||||
"cookingTime": 0,
|
||||
"remainingTime": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"burnerNumber": 1,
|
||||
"mode": "normal",
|
||||
"operationState": "ready",
|
||||
"powerLevel": "0",
|
||||
"hotSurfaceState": "normal",
|
||||
"panDetection": true,
|
||||
"timer": {
|
||||
"operationState": "ready",
|
||||
"cookingTime": 0,
|
||||
"remainingTime": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"burnerNumber": 2,
|
||||
"mode": "normal",
|
||||
"operationState": "ready",
|
||||
"powerLevel": "0",
|
||||
"hotSurfaceState": "normal",
|
||||
"panDetection": true,
|
||||
"timer": {
|
||||
"operationState": "ready",
|
||||
"cookingTime": 0,
|
||||
"remainingTime": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "-500",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePower": "212700",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "TP1X_DA-KS-COOKTOP-01011|40459741|50000203001711000A00000000000000",
|
||||
"x.com.samsung.da.description": "NV8500T-/KO4",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "26033000",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "04597A23021100, 04596A23080300",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
],
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "KCA",
|
||||
"x.com.samsung.da.diagTsId": "DA01"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"x.com.samsung.da.newVersionNo": "00000000",
|
||||
"x.com.samsung.da.currentVersionInfo": "00000000",
|
||||
"flashingProgress": "",
|
||||
"otnTarget": "main",
|
||||
"otnStatus": "None",
|
||||
"otnCompleteDate": "noHistory",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "AKS-WW-TP1-21-COOKTOP-2",
|
||||
"versions": [
|
||||
"40260330"
|
||||
],
|
||||
"visVersion": "260330"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "05114045974140459641",
|
||||
"versions": [
|
||||
"23021100",
|
||||
"23080300"
|
||||
],
|
||||
"visVersion": "230803"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Seoul",
|
||||
"offset": "+09:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+278
File diff suppressed because one or more lines are too long
+322
File diff suppressed because one or more lines are too long
+201
File diff suppressed because one or more lines are too long
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/bespoke/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.BespokeProduct": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/defrost/delay/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/defrost/delay/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/diagnosis/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/door/cooler/0",
|
||||
"rep": {
|
||||
"openState": "Close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/door/onedoorfreezer/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.openState": "Close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/doors/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Door",
|
||||
"x.com.samsung.da.openState": "Close"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "3",
|
||||
"x.com.samsung.da.description": "Door",
|
||||
"x.com.samsung.da.openState": "Close"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+01:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/icemaker/status/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/icemaker/status/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "ARTIK051_DONGLE_REF|00090741|00090028001130300100000000000000",
|
||||
"x.com.samsung.da.description": "ARTIK_REF_17K",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "WiFi Module",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "011120190711",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Micom",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "14040703,ffffffff",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/0",
|
||||
"rep": {
|
||||
"supportedModes": [
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"RVACATION_ON",
|
||||
"RVACATION_OFF"
|
||||
],
|
||||
"modes": [
|
||||
"RVACATION_OFF"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"RVACATION_ON",
|
||||
"RVACATION_OFF"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"RVACATION_OFF"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "Micom",
|
||||
"x.com.samsung.da.newVersionAvailable": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/refrigeration/0",
|
||||
"rep": {
|
||||
"defrost": false,
|
||||
"rapidFreeze": false,
|
||||
"rapidCool": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/refrigeration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.rapidFridge": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sabbath/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/current/cooler/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
1.0,
|
||||
7.0
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 3.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/cooler/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
1.0,
|
||||
7.0
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 3.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Fridge",
|
||||
"x.com.samsung.da.desired": "3",
|
||||
"x.com.samsung.da.current": "3",
|
||||
"x.com.samsung.da.maximum": "7",
|
||||
"x.com.samsung.da.minimum": "1",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/bespoke/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.BespokeProduct": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/defrost/delay/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/defrost/delay/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/diagnosis/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/door/onedoorfreezer/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.openState": "Close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/doors/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "3",
|
||||
"x.com.samsung.da.description": "Door",
|
||||
"x.com.samsung.da.openState": "Close"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+02:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/icemaker/status/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/icemaker/status/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "ARTIK051_DONGLE_REF|00127641|00080020001430300100000000000000",
|
||||
"x.com.samsung.da.description": "ARTIK_REF_17K",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "WiFi Module",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "011120190711",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Micom",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "2103120d,ffffffff",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/0",
|
||||
"rep": {
|
||||
"supportedModes": [
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"ENERGY_REPORT_MODEL",
|
||||
"18K_REF_OUTDOOR_CONTROL_V2"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"ENERGY_REPORT_MODEL",
|
||||
"18K_REF_OUTDOOR_CONTROL_V2"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "Micom",
|
||||
"x.com.samsung.da.newVersionAvailable": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/refrigeration/0",
|
||||
"rep": {
|
||||
"defrost": false,
|
||||
"rapidFreeze": false,
|
||||
"rapidCool": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/refrigeration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.rapidFreezing": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sabbath/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/current/freezer/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
-23.0,
|
||||
-15.0
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": -20.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/freezer/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
-23.0,
|
||||
-15.0
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": -20.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Freezer",
|
||||
"x.com.samsung.da.desired": "-20",
|
||||
"x.com.samsung.da.current": "-20",
|
||||
"x.com.samsung.da.maximum": "-15",
|
||||
"x.com.samsung.da.minimum": "-23",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.timeforlongnoti": "false",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cumulativePower": "915",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "A-VSKR-TP1-22-VS9500AL|50023541|80030100001611000800000000000000",
|
||||
"x.com.samsung.da.description": "A-VSKR-TP1-22-VS9500AL",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "VS1",
|
||||
"x.com.samsung.da.diagMinVersion": "1.0",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "023741250507",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "25050800",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+09:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "2650000000",
|
||||
"x.com.samsung.da.airconOptionList": [
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"CLEAN_OPTION_SUPPORT"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/component/station/dustbag/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "normal",
|
||||
"x.com.samsung.da.supportedStatus": [
|
||||
"full",
|
||||
"normal"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/setting/dustbin/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.autoEmpty": "On",
|
||||
"x.com.samsung.da.autoClose": "On",
|
||||
"x.com.samsung.da.desiredDischargingTime": "3",
|
||||
"x.com.samsung.da.supportedDischargingTime": [
|
||||
"1",
|
||||
"3"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/status/cleanstation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Ready",
|
||||
"x.com.samsung.da.uvcTotalOperationTime": "90",
|
||||
"x.com.samsung.da.uvcOperationTime": "90",
|
||||
"x.com.samsung.da.uvcFinishedTime": "2026-07-27T10:26Z",
|
||||
"x.com.samsung.da.emittedTime": "2026-07-26T12:26Z",
|
||||
"x.com.samsung.da.uvcIntensive": "Off",
|
||||
"x.com.samsung.da.stickStatus": "On"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/component/station/dustbagusage/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.dustbagUsage": "506",
|
||||
"x.com.samsung.da.dustbagPrevUsage": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"flashingProgress": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Seoul",
|
||||
"offset": "+09:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connectionconfig/vs/0",
|
||||
"rep": {
|
||||
"autoReconnectionMinVersion": "1.0",
|
||||
"autoReconnection": "true",
|
||||
"autoReconnectionProtocolType": [
|
||||
"helper_hotspot",
|
||||
"ble_ocf"
|
||||
],
|
||||
"supportedWiFiAuthType": [
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA-PSK",
|
||||
"WPA2-PSK",
|
||||
"SAE"
|
||||
],
|
||||
"supportedWiFiCryptoType": [
|
||||
"TKIP",
|
||||
"AES",
|
||||
"WEP-64",
|
||||
"WEP-128"
|
||||
],
|
||||
"supportedWiFiFreq": [
|
||||
"2.4G"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/diagnosis/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "-500",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePower": "40500",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.cumulativeDate": "1784988000",
|
||||
"x.com.samsung.da.cumulativeDateUTC": "1785013200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/course/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"HOMECARE_WIZARD_V2"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"DeviceType_0145",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"Course_01",
|
||||
"LaundryOutTime_0",
|
||||
"AddGarmentIndicator_Off",
|
||||
"SeamlessControl_Disable",
|
||||
"KidsLockBypass_On",
|
||||
"WashingTimes_18",
|
||||
"DrumCleanProposal_20",
|
||||
"DetergentOnce_0",
|
||||
"DetergentLeft_0",
|
||||
"DetergentBase_0",
|
||||
"DetergentAlarm_Off",
|
||||
"DetergentType_0",
|
||||
"DetergentTotal_0",
|
||||
"SoftenerOnce_0",
|
||||
"SoftenerLeft_0",
|
||||
"SoftenerBase_0",
|
||||
"SoftenerAlarm_Off",
|
||||
"SoftenerType_0",
|
||||
"SoftenerTotal_0",
|
||||
"SpecialFunction_6",
|
||||
"AvailableDelayTime_67",
|
||||
"DetergentLevelCtrl_0",
|
||||
"SoftenerLevelCtrl_3",
|
||||
"SupportedDetergentLevelCtrl_00010203",
|
||||
"SupportedSoftenerLevelCtrl_00010203",
|
||||
"DetergentLevel2Ctrl_1",
|
||||
"SoftenerLevel2Ctrl_1",
|
||||
"SupportedDetergentLevel2Ctrl_010203",
|
||||
"SupportedSoftenerLevel2Ctrl_010203",
|
||||
"LaundryPlannerUserSetTime_0",
|
||||
"ProgressTimeSet_4205DC8208E8A206AE",
|
||||
"SuperSpeed_Off",
|
||||
"SendToDevice_Off",
|
||||
"DetergentCycleUsed_0",
|
||||
"SoftenerCycleUsed_0",
|
||||
"CloudCourse_001C590549164D114A224C2037F0AC22",
|
||||
"CloudExtraCourse_5958",
|
||||
"OneTimeCloudCourse_FFFF010049004D004A804C0037F0AC00",
|
||||
"EnergyLevelSet_05020403040204040401030203",
|
||||
"MostUsed_01843E913FA43BC33E",
|
||||
"SuperSpeedSet_F0F000F00000000000000000",
|
||||
"DispenserSet_22222222222222FFF2000022",
|
||||
"GeoFenceAlarm",
|
||||
"UsagesDB_ok",
|
||||
"EnergyKW_396",
|
||||
"DrumCleanLog_2025-09-07T18:15:52|2025-10-05T18:49:21|2025-11-23T18:20:07|2026-01-01T19:11:59|2026-02-15T03:36:21|2026-03-09T19:59:19|2026-04-19T21:35:33|2026-05-10T19:21:00|2026-06-12T21:32:16|2026-06-29T06:29:50",
|
||||
"TimeSync_NotSupported"
|
||||
],
|
||||
"x.com.samsung.da.supportedOptions": [
|
||||
"401843E913FA43BC33E53853E913FA43FC53E86843E913FA43FC33E7C853E923FA43FC53E65821E913FA43FC33E7D843E923FA31FC33E1783089102A410C3087E85209204A308C0005E8000913FA43FC00055831E913FA31FC20E57821E913FA20FC10204843E913FA43FC13E"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cycleinterface/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.kidsLock": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.state": "Ready",
|
||||
"x.com.samsung.da.remainingTime": "01:07:00",
|
||||
"x.com.samsung.da.progressPercentage": "1",
|
||||
"x.com.samsung.da.progress": "None",
|
||||
"x.com.samsung.da.delayEndTime": "00:00:00",
|
||||
"x.com.samsung.da.supportedProgress": [
|
||||
"None",
|
||||
"Weightsensing",
|
||||
"Wash",
|
||||
"Rinse",
|
||||
"Spin",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/0",
|
||||
"rep": {
|
||||
"currentMachineState": "**REDACTED**",
|
||||
"machineStates": "**REDACTED**",
|
||||
"jobStates": [
|
||||
"None",
|
||||
"Weightsensing",
|
||||
"Wash",
|
||||
"Rinse",
|
||||
"Spin",
|
||||
"Finish"
|
||||
],
|
||||
"currentJobState": "None",
|
||||
"remainingTime": "01:07:00",
|
||||
"progressPercentage": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "DA_WM_TP1_21_COMMON|20233742|20000001001511200AAB020200000000",
|
||||
"x.com.samsung.da.description": "DA_WM_TP1_21_COMMON_WA7700A/DC92-03005B_004A",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "210",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagTsId": "DA01",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "DA_WM_TP1_21_COMMON|20233742|20000001001511200AAB020200000000",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02986A260118(A182)",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Firmware_1_DB_20233742210931500FFFFF203005422212139101FFFF(01452023374220300542_30221213)(FileDown:0)(Type:0)",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "02337B21093150,03005B22121391",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Firmware_2_DB_2023394522081051012FFFFFFFFFFFFFFFFFFFFFFFFE(014520233945FFFFFFFF_30000000)(FileDown:0)(Type:0)",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "02339E22081051,FFFFFFFFFFFFFF"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "-07:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/washer/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.detergentLevel": "1",
|
||||
"x.com.samsung.da.softenerLevel": "2",
|
||||
"x.com.samsung.da.waterTemperature": "Warm",
|
||||
"x.com.samsung.da.supportedWaterTemperature": [
|
||||
"None",
|
||||
"Cold",
|
||||
"Cool",
|
||||
"EcoWarm",
|
||||
"Warm",
|
||||
"Hot"
|
||||
],
|
||||
"x.com.samsung.da.spinLevel": "High",
|
||||
"x.com.samsung.da.supportedSpinLevel": [
|
||||
"RinseHold",
|
||||
"NoSpin",
|
||||
"Low",
|
||||
"Medium",
|
||||
"High",
|
||||
"ExtraHigh"
|
||||
],
|
||||
"x.com.samsung.da.soilLevel": "Normal",
|
||||
"x.com.samsung.da.supportedSoilLevel": [
|
||||
"None",
|
||||
"ExtraLight",
|
||||
"Light",
|
||||
"Normal",
|
||||
"Heavy",
|
||||
"ExtraHeavy"
|
||||
],
|
||||
"x.com.samsung.da.rinseCycles": "1",
|
||||
"x.com.samsung.da.supportedRinseCycles": [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/st/washercourse/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.st.washerMode": "Table_02_Course_01",
|
||||
"x.com.samsung.da.st.courseTable": "Table_02"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/setting/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/wm/editcourse/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/wm/setinfo/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.isModelSettingWithoutSC": "true",
|
||||
"x.com.samsung.da.isModelSettingPowerOnOff": "false",
|
||||
"x.com.samsung.da.modelCode": "M(None),W(WA55A7700AV/US)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/jobbeginingstatus/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.currentStatus": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"x.com.samsung.da.newVersionNo": "00000000",
|
||||
"x.com.samsung.da.currentVersionInfo": "00000000",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "",
|
||||
"otnTarget": "main",
|
||||
"otnCompleteDate": "2026-03-04",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "DA_WM_TP1_21_COMMON",
|
||||
"versions": [
|
||||
"30260118"
|
||||
],
|
||||
"visVersion": "260118"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "01452023374220300542",
|
||||
"versions": [
|
||||
"21093150",
|
||||
"22121391"
|
||||
],
|
||||
"visVersion": "221213"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "014520233945FFFFFFFF",
|
||||
"versions": [
|
||||
"22081051",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "220810"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.remoteControlEnabled": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "0000000000",
|
||||
"x.com.samsung.da.countryCode": "US"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "America/Phoenix",
|
||||
"offset": "-07:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connectionconfig/vs/0",
|
||||
"rep": {
|
||||
"autoReconnectionMinVersion": "1.0",
|
||||
"autoReconnection": "true",
|
||||
"autoReconnectionProtocolType": [
|
||||
"helper_hotspot",
|
||||
"ble_ocf"
|
||||
],
|
||||
"supportedWiFiAuthType": [
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA-PSK",
|
||||
"WPA2-PSK",
|
||||
"SAE"
|
||||
],
|
||||
"supportedWiFiCryptoType": [
|
||||
"TKIP",
|
||||
"AES",
|
||||
"WEP-64",
|
||||
"WEP-128"
|
||||
],
|
||||
"supportedWiFiFreq": [
|
||||
"2.4G"
|
||||
],
|
||||
"calmConnectionCare": {
|
||||
"version": "1.0",
|
||||
"role": [
|
||||
"things"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wirelessinfo/vs/0",
|
||||
"rep": {
|
||||
"macaddressWiFi": "**REDACTED**",
|
||||
"macaddressBLE": "**REDACTED**"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/quickcontrol/info/vs/0",
|
||||
"rep": {
|
||||
"supportedVersion": "1.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/diagnosis/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "-500",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePower": "121100",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.cumulativeDate": "1785142800",
|
||||
"x.com.samsung.da.cumulativeDateUTC": "1785110400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/course/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"HOMECARE_WIZARD_V2"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"DeviceType_015C",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"Course_01",
|
||||
"LaundryOutTime_0",
|
||||
"SeamlessControl_Enable",
|
||||
"KidsLockBypass_On",
|
||||
"FreezeProtectionAlarm_On",
|
||||
"WashingTimes_19",
|
||||
"DrumCleanProposal_20",
|
||||
"DrumCleanLog_2026-02-04T04:05:54|2026-02-28T08:48:43|2026-03-16T03:15:29|2026-03-26T02:55:05|2026-04-15T05:57:11|2026-04-30T05:37:44|2026-05-14T05:29:20|2026-06-04T01:55:39|2026-06-25T05:21:23|2026-07-11T03:10:21",
|
||||
"DetergentOnce_0",
|
||||
"DetergentLeft_0",
|
||||
"DetergentBase_0",
|
||||
"DetergentAlarm_Off",
|
||||
"DetergentType_0",
|
||||
"DetergentTotal_0",
|
||||
"SoftenerOnce_0",
|
||||
"SoftenerLeft_0",
|
||||
"SoftenerBase_0",
|
||||
"SoftenerAlarm_Off",
|
||||
"SoftenerType_0",
|
||||
"SoftenerTotal_0",
|
||||
"SpecialFunction_4",
|
||||
"AvailableDelayTime_73",
|
||||
"LaundryPlannerUserSetTime_0",
|
||||
"ProgressTimeSet_420780820438A205BE",
|
||||
"FreezeProtectionMode_Off",
|
||||
"SendToDevice_Off",
|
||||
"EnergyLevelSet_0503030303040304010505020204",
|
||||
"MostUsed_01923FA33F510E60A17340",
|
||||
"GeoFenceAlarm",
|
||||
"UsagesDB_ok",
|
||||
"EnergyKW_396",
|
||||
"DrumCleanLog_2026-02-04T04:05:54|2026-02-28T08:48:43|2026-03-16T03:15:29|2026-03-26T02:55:05|2026-04-15T05:57:11|2026-04-30T05:37:44|2026-05-14T05:29:20|2026-06-04T01:55:39|2026-06-25T05:21:23|2026-07-11T03:10:21",
|
||||
"TimeSync_NotSupported"
|
||||
],
|
||||
"x.com.samsung.da.supportedOptions": [
|
||||
"501923FA33F510E60A1734050923FA33F510E60A173400F933FA33F510E60A173407B923FA31F520E60A1724006933FA31F510E68A172400C933FA31F520E60A172404F9204A41052046666733308913FA33F510E60A170004A923FA31F520C645175554B923FA31F520C64517555269204A102510664A171114C913FA31F510E624171404D9204A410520466667555"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cycleinterface/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.kidsLock": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.state": "Ready",
|
||||
"x.com.samsung.da.remainingTime": "01:13:00",
|
||||
"x.com.samsung.da.progressPercentage": "1",
|
||||
"x.com.samsung.da.progress": "None",
|
||||
"x.com.samsung.da.delayEndTime": "00:00:00",
|
||||
"x.com.samsung.da.supportedProgress": [
|
||||
"None",
|
||||
"Weightsensing",
|
||||
"Wash",
|
||||
"Rinse",
|
||||
"Spin",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/0",
|
||||
"rep": {
|
||||
"currentMachineState": "**REDACTED**",
|
||||
"machineStates": "**REDACTED**",
|
||||
"jobStates": [
|
||||
"None",
|
||||
"Weightsensing",
|
||||
"Wash",
|
||||
"Rinse",
|
||||
"Spin",
|
||||
"Finish"
|
||||
],
|
||||
"currentJobState": "None",
|
||||
"remainingTime": "01:13:00",
|
||||
"progressPercentage": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "DA_WM_TP2_20_COMMON|20233741|200000010014110002A3020200000000",
|
||||
"x.com.samsung.da.description": "DA_WM_TP2_20_COMMON_WA8000T/DC92-02810A_0002",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "210",
|
||||
"x.com.samsung.da.diagMinVersion": "1.0",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "DA_WM_TP2_20_COMMON|20233741|200000010014110002A3020200000000",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02674A250414(F546)",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Firmware_1_DB_20233741181128160FFFFF202810412011303403FFFF(015C2023374120281041_30000000)(FileDown:0)(Type:0)",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "18112816,20113034",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Firmware_2_DB_2023394400000000032FFFFFFFFFFFFFFFFFFFFFFFFE(015C20233944FFFFFFFF_30000000)(FileDown:0)(Type:0)",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "00000000,FFFFFFFF"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+09:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/washer/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.spinLevel": "Medium",
|
||||
"x.com.samsung.da.supportedSpinLevel": [
|
||||
"NoSpin",
|
||||
"Delicate",
|
||||
"Low",
|
||||
"Medium",
|
||||
"High",
|
||||
"ExtraHigh"
|
||||
],
|
||||
"x.com.samsung.da.rinseCycles": "2",
|
||||
"x.com.samsung.da.supportedRinseCycles": [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
],
|
||||
"x.com.samsung.da.waterValve": "Cold",
|
||||
"x.com.samsung.da.supportedWaterValve": [
|
||||
"None",
|
||||
"Cold",
|
||||
"Warm",
|
||||
"Hot"
|
||||
],
|
||||
"x.com.samsung.da.waterHeight": "0",
|
||||
"x.com.samsung.da.supportedWaterHeight": [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"NoDisplay"
|
||||
],
|
||||
"x.com.samsung.da.washTime": "21",
|
||||
"x.com.samsung.da.supportedWashTime": [
|
||||
"0",
|
||||
"9",
|
||||
"15",
|
||||
"21",
|
||||
"27",
|
||||
"50+"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/st/washercourse/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.st.washerMode": "Table_02_Course_01",
|
||||
"x.com.samsung.da.st.courseTable": "Table_02"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/setting/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/wm/editcourse/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/wm/setinfo/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.isModelSettingWithoutSC": "true",
|
||||
"x.com.samsung.da.isModelSettingPowerOnOff": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/jobbeginingstatus/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.currentStatus": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "Micom",
|
||||
"x.com.samsung.da.newVersionAvailable": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.remoteControlEnabled": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "4137000000",
|
||||
"x.com.samsung.da.countryCode": "KR"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/selfcheck/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Ready",
|
||||
"x.com.samsung.da.result": "Success",
|
||||
"x.com.samsung.da.error": [
|
||||
"DA_ERROR_NONE"
|
||||
],
|
||||
"x.com.samsung.da.supportedActions": [
|
||||
"Start"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"HOMECARE_WIZARD_V2"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"WATERFILTER_DISABLE"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "TP2X_WATERPURIFIER_20K|00156641|900000000216130001088700000E0000",
|
||||
"x.com.samsung.da.description": "TP2X_WATERPURIFIER_20K",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "RP0",
|
||||
"x.com.samsung.da.diagMinVersion": "1.0",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "WiFi Module",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02144A240904",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Micom",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "24091109, 2409110D, 22092302, FFFFFFFF, 23030200, FFFFFFFF",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+00:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "A-WPWW-TP2-22-COMMON",
|
||||
"versions": [
|
||||
"20240904"
|
||||
],
|
||||
"visVersion": "240904"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "03800015664100156041",
|
||||
"versions": [
|
||||
"24091109",
|
||||
"2409110D"
|
||||
],
|
||||
"visVersion": "240911"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "038000156141FFFFFFFF",
|
||||
"versions": [
|
||||
"22092302",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "220923"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "038000159141FFFFFFFF",
|
||||
"versions": [
|
||||
"23030200",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "230302"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/waterfilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsage": "90",
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterStatus": "normal",
|
||||
"x.com.samsung.da.lastResetDate": "2025-08-29T03:22:31",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"replaceable"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/status/lock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.coldwaterLock": "Unlocked",
|
||||
"x.com.samsung.da.buzzLock": "Unlocked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/setting/waterpurifier/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.hotwaterLevel": "5",
|
||||
"x.com.samsung.da.desiredType": "ambientwater",
|
||||
"x.com.samsung.da.tempDesiredHotWater": "40",
|
||||
"x.com.samsung.da.tempUnit": "C",
|
||||
"x.com.samsung.da.desiredCapacity": "100",
|
||||
"x.com.samsung.da.capacityUnit": "C",
|
||||
"x.com.samsung.da.capacityResolution": "10",
|
||||
"x.com.samsung.da.hotwaterRange": [
|
||||
"40",
|
||||
"90"
|
||||
],
|
||||
"x.com.samsung.da.desiredCapacityRange": [
|
||||
"50",
|
||||
"2000"
|
||||
],
|
||||
"x.com.samsung.da.supportedTypes": [
|
||||
"ambientwater",
|
||||
"coldwater",
|
||||
"hotwater"
|
||||
],
|
||||
"x.com.samsung.da.pourStatus": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/status/waterpurifier/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Ready",
|
||||
"x.com.samsung.da.filterDoorStatus": "Close",
|
||||
"x.com.samsung.da.sterilizeRunTime": "0",
|
||||
"x.com.samsung.da.sterilizeStartTime": "2026-07-27T17:00:00",
|
||||
"x.com.samsung.da.sterilizePeriod": "3",
|
||||
"x.com.samsung.da.sterilizePlanTime": "2026-07-27T17:00:00",
|
||||
"x.com.samsung.da.sterilizeLastTime": "2026-07-24T17:08:25",
|
||||
"x.com.samsung.da.sterilizeMaxTime": "10",
|
||||
"x.com.samsung.da.filterCleanRemainTime": "0",
|
||||
"x.com.samsung.da.filterCleanMaxTime": "14"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/favorite/capacity/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.switchCapacity": "On",
|
||||
"x.com.samsung.da.defaultCapacity": "100",
|
||||
"x.com.samsung.da.capacityList": [
|
||||
"100",
|
||||
"200",
|
||||
"260",
|
||||
"500",
|
||||
"9999"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/favorite/hotwater/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.switchHotwater": "Unlocked",
|
||||
"x.com.samsung.da.favorite.defaultTemperature": "40",
|
||||
"x.com.samsung.da.favorite.revision": "0",
|
||||
"x.com.samsung.da.favorite.showList": [
|
||||
"40",
|
||||
"75",
|
||||
"85",
|
||||
"90"
|
||||
],
|
||||
"x.com.samsung.da.favorite.supportedList": [
|
||||
"40",
|
||||
"75",
|
||||
"85",
|
||||
"90"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/automation/waterpurifier/vs/0",
|
||||
"rep": {
|
||||
"automation.supported.modes": [
|
||||
"0"
|
||||
],
|
||||
"automation.supported.options": [
|
||||
"capacity"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Seoul",
|
||||
"offset": "+09:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/recipe/coffee/vs/0",
|
||||
"rep": {
|
||||
"publisher.support": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/coffee/custom/recipe/vs/0",
|
||||
"rep": {
|
||||
"publisher.support": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/favorite/coffee/vs/0",
|
||||
"rep": {
|
||||
"favorite.activate": "On",
|
||||
"brew.status": "Suspend",
|
||||
"favorite.enable.list": [
|
||||
"0",
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/recipe/coffee/deletion/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/brand/recipe/info/vs/0",
|
||||
"rep": {
|
||||
"revision": "0",
|
||||
"brand.num.max": "1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/selfcheck/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Ready",
|
||||
"x.com.samsung.da.result": "Success",
|
||||
"x.com.samsung.da.error": [
|
||||
"DA_ERROR_NONE"
|
||||
],
|
||||
"x.com.samsung.da.supportedActions": [
|
||||
"Start"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"HOMECARE_WIZARD_V2"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"WATERFILTER_DISABLE"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/0",
|
||||
"rep": {
|
||||
"supportedModes": [
|
||||
"HOMECARE_WIZARD_V2"
|
||||
],
|
||||
"modes": [
|
||||
"WATERFILTER_DISABLE"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "TP2X_WATERPURIFIER_20K|00132341|900000000215130001060F0000020000",
|
||||
"x.com.samsung.da.description": "TP2X_WATERPURIFIER_20K",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "540",
|
||||
"x.com.samsung.da.diagMinVersion": "1.0",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "WiFi Module",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02144A240615",
|
||||
"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": "24061111, 24091218, 21072807, FFFFFFFF",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+09:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "A-WPWW-TP2-20-COMMON",
|
||||
"versions": [
|
||||
"20240615"
|
||||
],
|
||||
"visVersion": "240615"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "03800013234100132441",
|
||||
"versions": [
|
||||
"24061111",
|
||||
"24091218"
|
||||
],
|
||||
"visVersion": "240912"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "038000132541FFFFFFFF",
|
||||
"versions": [
|
||||
"21072807",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "210728"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/waterfilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsage": "28",
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterStatus": "normal",
|
||||
"x.com.samsung.da.lastResetDate": "2026-04-14T14:33:15",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"replaceable"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/status/lock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.hotwaterLock": "Unlocked",
|
||||
"x.com.samsung.da.coldwaterLock": "Unlocked",
|
||||
"x.com.samsung.da.buzzLock": "Unlocked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/setting/waterpurifier/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.desiredType": "coldwater",
|
||||
"x.com.samsung.da.tempDesiredHotWater": "85",
|
||||
"x.com.samsung.da.tempUnit": "C",
|
||||
"x.com.samsung.da.desiredCapacity": "100",
|
||||
"x.com.samsung.da.capacityUnit": "C",
|
||||
"x.com.samsung.da.capacityResolution": "10",
|
||||
"x.com.samsung.da.desiredCapacityRange": [
|
||||
"50",
|
||||
"2000"
|
||||
],
|
||||
"x.com.samsung.da.supportedTypes": [
|
||||
"ambientwater",
|
||||
"coldwater",
|
||||
"hotwater"
|
||||
],
|
||||
"x.com.samsung.da.supportedHotTemperatures": [
|
||||
"40",
|
||||
"75",
|
||||
"85"
|
||||
],
|
||||
"x.com.samsung.da.triggerTime": "1785063153",
|
||||
"x.com.samsung.da.pourStatus": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/status/waterpurifier/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Ready",
|
||||
"x.com.samsung.da.filterDoorStatus": "Close",
|
||||
"x.com.samsung.da.sterilizeRunTime": "0",
|
||||
"x.com.samsung.da.sterilizeStartTime": "2026-07-27T18:00:00",
|
||||
"x.com.samsung.da.sterilizePeriod": "3",
|
||||
"x.com.samsung.da.sterilizePlanTime": "2026-07-27T18:00:00",
|
||||
"x.com.samsung.da.sterilizeLastTime": "2026-07-24T18:09:25",
|
||||
"x.com.samsung.da.sterilizeMaxTime": "10",
|
||||
"x.com.samsung.da.filterCleanRemainTime": "0",
|
||||
"x.com.samsung.da.filterCleanMaxTime": "15"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/favorite/capacity/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.switchCapacity": "On",
|
||||
"x.com.samsung.da.defaultCapacity": "100",
|
||||
"x.com.samsung.da.maxCapacity": "6",
|
||||
"x.com.samsung.da.capacityList": [
|
||||
"100",
|
||||
"150",
|
||||
"200",
|
||||
"250",
|
||||
"550",
|
||||
"9999"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/automation/waterpurifier/vs/0",
|
||||
"rep": {
|
||||
"automation.supported.modes": [
|
||||
"0"
|
||||
],
|
||||
"automation.supported.options": [
|
||||
"capacity"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Seoul",
|
||||
"offset": "+09:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -508,3 +508,19 @@ async def test_options_flow_finish_preserves_existing_options(
|
||||
|
||||
assert result['type'] == FlowResultType.CREATE_ENTRY
|
||||
assert entry.options[CONF_BYPASS_REMOTE_CONTROL] is True
|
||||
|
||||
|
||||
def test_is_placeholder_serial_catches_nothing_svc():
|
||||
"""Issue #83: the ARTIK051_DONGLE_REF firmware family reports the
|
||||
literal string 'Nothing(SVC)' as serialNum on every unit -- non-empty,
|
||||
so it must be caught by name, not by the plain `if not serial` check."""
|
||||
from custom_components.localthings.config_flow import _is_placeholder_serial
|
||||
assert _is_placeholder_serial('Nothing(SVC)') is True
|
||||
assert _is_placeholder_serial('nothing(svc)') is True
|
||||
assert _is_placeholder_serial(' Nothing(SVC) ') is True
|
||||
|
||||
|
||||
def test_is_placeholder_serial_accepts_real_serials():
|
||||
from custom_components.localthings.config_flow import _is_placeholder_serial
|
||||
assert _is_placeholder_serial('0A1B2C3D4E5F') is False
|
||||
assert _is_placeholder_serial('') is False
|
||||
|
||||
@@ -16,6 +16,7 @@ from custom_components.localthings.const import (
|
||||
from custom_components.localthings.coordinator import LocalThingsCoordinator
|
||||
from custom_components.localthings.registry.capabilities.common import (
|
||||
remote_control_enabled,
|
||||
remote_control_required_for_write,
|
||||
)
|
||||
from custom_components.localthings.observe import MODE_OBSERVE, MODE_POLL, PUSH_HEALTH_WINDOW_S
|
||||
|
||||
@@ -136,6 +137,29 @@ def test_run_discovery_detects_washer_via_model_fallback(
|
||||
assert coordinator.device_type_name == 'washer'
|
||||
|
||||
|
||||
def test_run_discovery_falls_back_to_host_for_placeholder_serial(
|
||||
hass: HomeAssistant, mock_entry
|
||||
) -> None:
|
||||
"""Issue #83: the ARTIK051_DONGLE_REF firmware family reports the
|
||||
literal string 'Nothing(SVC)' as serialNum on every unit. Left as-is,
|
||||
two such units get the same device_serial (which feeds both the HA
|
||||
device-registry identifier and every entity's unique_id), so the
|
||||
second one's entities silently collide and get dropped. It must be
|
||||
treated the same as an empty serial and fall back to the host."""
|
||||
resources = {
|
||||
'/information/vs/0': {
|
||||
'x.com.samsung.da.modelNum':
|
||||
'ARTIK051_DONGLE_REF|00127641|00080020001430300100000000000000',
|
||||
'x.com.samsung.da.description': 'ARTIK_REF_17K',
|
||||
'x.com.samsung.da.serialNum': 'Nothing(SVC)',
|
||||
},
|
||||
'/otninformation/vs/0': {},
|
||||
}
|
||||
coordinator = LocalThingsCoordinator(hass, mock_entry)
|
||||
coordinator._run_discovery(resources)
|
||||
assert coordinator.device_serial == mock_entry.data[CONF_HOST]
|
||||
|
||||
|
||||
def test_run_discovery_detects_cooktop_via_resource_signature(
|
||||
hass: HomeAssistant, mock_entry
|
||||
) -> None:
|
||||
@@ -145,7 +169,7 @@ def test_run_discovery_detects_cooktop_via_resource_signature(
|
||||
coordinator = LocalThingsCoordinator(hass, mock_entry)
|
||||
coordinator._run_discovery(_load_device('cooktop'))
|
||||
|
||||
assert coordinator.device_type_name == 'cooktop'
|
||||
assert coordinator.device_type_name == 'gas_cooktop'
|
||||
assert coordinator._unbound_hrefs == []
|
||||
|
||||
|
||||
@@ -722,10 +746,10 @@ async def test_climate_power_write_applies_to_its_own_href_not_bound_href(
|
||||
"""Regression for issues #17/#53, using the real AC capability. The
|
||||
composite climate entity binds /mode/vs/0 (airconditioner.CLIMATE.href),
|
||||
but a power command's write_fn (airconditioner._climate_write) targets
|
||||
the sibling /power/0 -- the href climate.py's `_is_on()` actually reads.
|
||||
the sibling /power/vs/0 -- the href climate.py's `_is_on()` actually reads.
|
||||
The optimistic value and settle guard must land there, not on the bound
|
||||
/mode/vs/0, or the entity never sees the write and shows stale state
|
||||
until the next unrelated read of /power/0."""
|
||||
until the next unrelated read of /power/vs/0."""
|
||||
from custom_components.localthings.registry.capabilities import airconditioner
|
||||
from custom_components.localthings.registry.discovery import BoundEntity
|
||||
|
||||
@@ -744,9 +768,11 @@ async def test_climate_power_write_applies_to_its_own_href_not_bound_href(
|
||||
fake.post = lambda *a, **k: (0x44, b'')
|
||||
await coordinator.async_send_command(bound, ('power', True))
|
||||
|
||||
assert coordinator._cache.get('/power/0') == {'value': True}
|
||||
assert coordinator._cache.get(airconditioner.CLIMATE.href) != {'value': True}
|
||||
assert coordinator._observe._settle_until.get('/power/0') is not None
|
||||
assert (coordinator._cache.get('/power/vs/0') or {}).get(
|
||||
'x.com.samsung.da.power') == 'On'
|
||||
assert 'x.com.samsung.da.power' not in (
|
||||
coordinator._cache.get(airconditioner.CLIMATE.href) or {})
|
||||
assert coordinator._observe._settle_until.get('/power/vs/0') is not None
|
||||
|
||||
|
||||
async def test_send_command_survives_stale_confirm_poll(
|
||||
@@ -880,6 +906,38 @@ class TestRemoteControlEnabled:
|
||||
assert remote_control_enabled({}) is True
|
||||
|
||||
|
||||
class TestRemoteControlRequiredForWrite:
|
||||
"""isModelSettingWithoutSC auto-exempts settings writes, not cycle control."""
|
||||
|
||||
def test_without_flag_always_requires(self):
|
||||
resources = {}
|
||||
assert remote_control_required_for_write(resources, '/washer/vs/0') is True
|
||||
assert remote_control_required_for_write(
|
||||
resources, '/operational/state/vs/0') is True
|
||||
|
||||
def test_without_sc_exempts_settings_keeps_operational(self):
|
||||
resources = {
|
||||
'/wm/setinfo/vs/0': {
|
||||
'x.com.samsung.da.isModelSettingWithoutSC': 'true',
|
||||
},
|
||||
}
|
||||
assert remote_control_required_for_write(resources, '/washer/vs/0') is False
|
||||
assert remote_control_required_for_write(resources, '/course/vs/0') is False
|
||||
assert remote_control_required_for_write(resources, '/buzzersound/vs/0') is False
|
||||
assert remote_control_required_for_write(
|
||||
resources, '/operational/state/vs/0') is True
|
||||
assert remote_control_required_for_write(
|
||||
resources, '/operational/state/0') is True
|
||||
|
||||
def test_without_sc_false_still_requires(self):
|
||||
resources = {
|
||||
'/wm/setinfo/vs/0': {
|
||||
'x.com.samsung.da.isModelSettingWithoutSC': 'false',
|
||||
},
|
||||
}
|
||||
assert remote_control_required_for_write(resources, '/washer/vs/0') is True
|
||||
|
||||
|
||||
async def test_send_command_blocked_when_remote_control_disabled(
|
||||
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session
|
||||
) -> None:
|
||||
@@ -1024,3 +1082,95 @@ async def test_send_command_bypasses_remote_control_when_option_enabled(
|
||||
await coordinator.async_send_command(bound, 5)
|
||||
|
||||
assert coordinator._cache.get('/some/path') == {'value': 5}
|
||||
|
||||
|
||||
async def test_send_command_settings_allowed_when_without_sc(
|
||||
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session
|
||||
) -> None:
|
||||
"""Laundry boards with isModelSettingWithoutSC=true accept settings
|
||||
writes while remote control is off -- no options-flow bypass needed."""
|
||||
from custom_components.localthings.registry.discovery import BoundEntity
|
||||
from custom_components.localthings.registry.entities import SelectDesc
|
||||
|
||||
fake = mock_coordinator_observe_session
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
||||
coordinator._cache.apply_rep(
|
||||
'/remotectrl/vs/0',
|
||||
{'x.com.samsung.da.remoteControlEnabled': 'false'},
|
||||
source='test',
|
||||
)
|
||||
coordinator._cache.apply_rep(
|
||||
'/wm/setinfo/vs/0',
|
||||
{'x.com.samsung.da.isModelSettingWithoutSC': 'true'},
|
||||
source='test',
|
||||
)
|
||||
|
||||
def _write_fn(payload, rep, href=None):
|
||||
return (['washer', 'vs', '0'], {'x.com.samsung.da.spinLevel': payload})
|
||||
|
||||
desc = SelectDesc(key='spin_speed', field='x.com.samsung.da.spinLevel',
|
||||
write_fn=_write_fn)
|
||||
bound = BoundEntity(
|
||||
href='/washer/vs/0', capability=coordinator.bound[0].capability, desc=desc,
|
||||
)
|
||||
|
||||
with patch.object(fake, 'subscribe'):
|
||||
fake.post = lambda *a, **k: (0x44, b'')
|
||||
await coordinator.async_send_command(bound, '1000')
|
||||
|
||||
assert coordinator._cache.get('/washer/vs/0') == {
|
||||
'x.com.samsung.da.spinLevel': '1000',
|
||||
}
|
||||
|
||||
|
||||
async def test_send_command_operational_still_blocked_when_without_sc(
|
||||
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session
|
||||
) -> None:
|
||||
"""Start/Pause/Stop stay gated on Smart Control even when settings
|
||||
are allowed without it -- isModelSettingWithoutSC is settings-only."""
|
||||
from custom_components.localthings.registry.discovery import BoundEntity
|
||||
from custom_components.localthings.registry.entities import ButtonDesc
|
||||
|
||||
fake = mock_coordinator_observe_session
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
||||
coordinator._cache.apply_rep(
|
||||
'/remotectrl/vs/0',
|
||||
{'x.com.samsung.da.remoteControlEnabled': 'false'},
|
||||
source='test',
|
||||
)
|
||||
coordinator._cache.apply_rep(
|
||||
'/wm/setinfo/vs/0',
|
||||
{'x.com.samsung.da.isModelSettingWithoutSC': 'true'},
|
||||
source='test',
|
||||
)
|
||||
|
||||
def _write_fn(payload, rep, href=None):
|
||||
return (['operational', 'state', 'vs', '0'],
|
||||
{'x.com.samsung.da.state': payload})
|
||||
|
||||
desc = ButtonDesc(key='start', payload='Run', write_fn=_write_fn)
|
||||
bound = BoundEntity(
|
||||
href='/operational/state/vs/0',
|
||||
capability=coordinator.bound[0].capability,
|
||||
desc=desc,
|
||||
)
|
||||
|
||||
posted = False
|
||||
|
||||
def _post(*a, **k):
|
||||
nonlocal posted
|
||||
posted = True
|
||||
return (0x44, b'')
|
||||
|
||||
with patch.object(fake, 'subscribe'):
|
||||
fake.post = _post
|
||||
with pytest.raises(ServiceValidationError) as exc_info:
|
||||
await coordinator.async_send_command(bound, 'Run')
|
||||
|
||||
assert exc_info.value.translation_domain == DOMAIN
|
||||
assert exc_info.value.translation_key == 'remote_control_disabled'
|
||||
assert posted is False
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Tests for the /mode/vs/0 match_fn discriminator in capabilities/
|
||||
air_purifier.py (issue #130).
|
||||
|
||||
`_has_top_level_modes` decides whether a given air-purifier dump's shared
|
||||
/mode/vs/0 href binds to the older ARTIK051_TVTL family's MODE capability
|
||||
(packed options[] scheme) or the newer TP1X_DA-AC-AIR family's FAN
|
||||
capability (modes/supportedModes reported directly). Pure function, no
|
||||
coordinator/entity dependency needed to test it directly -- same rationale
|
||||
as test_climate_wind_oscillation_fallback.py's `_oscillation_swing`.
|
||||
"""
|
||||
from custom_components.localthings.registry.capabilities.air_purifier import (
|
||||
_has_top_level_modes,
|
||||
)
|
||||
|
||||
|
||||
def test_new_family_with_supported_modes_list_matches():
|
||||
rep = {
|
||||
'x.com.samsung.da.modes': ['Smart'],
|
||||
'x.com.samsung.da.supportedModes': ['Smart', 'Max', 'Mid', 'WindFree', 'Sleep'],
|
||||
}
|
||||
assert _has_top_level_modes(rep, {}) is True
|
||||
|
||||
|
||||
def test_old_family_with_no_supported_modes_field_does_not_match():
|
||||
"""The ARTIK051_TVTL family's /mode/vs/0 packs everything into
|
||||
options[] (Light_On, Comode_Off, ...) and has no top-level
|
||||
supportedModes at all."""
|
||||
rep = {
|
||||
'x.com.samsung.da.options': ['Light_On', 'Comode_Off', 'OptionCode_60282'],
|
||||
}
|
||||
assert _has_top_level_modes(rep, {}) is False
|
||||
|
||||
|
||||
def test_non_list_supported_modes_does_not_match():
|
||||
assert _has_top_level_modes({'x.com.samsung.da.supportedModes': 'Smart'}, {}) is False
|
||||
|
||||
|
||||
def test_empty_rep_does_not_match():
|
||||
assert _has_top_level_modes({}, {}) is False
|
||||
@@ -93,15 +93,30 @@ def test_air_filter_usage_is_percentage_of_capacity():
|
||||
|
||||
|
||||
def test_climate_write_targets():
|
||||
"""The CLIMATE write_fn maps each (kind, value) command to the right OCF
|
||||
POST target and body. `value` is already the raw device code."""
|
||||
"""The CLIMATE write_fn maps each (kind, value) command to the right vendor
|
||||
POST target and body. `value` is already the raw device code. Power and
|
||||
temperature target the vendor /power/vs/0 and /temperatures/vs/0 (the OCF
|
||||
/power/0 is absent on most boards and a non-authoritative mirror where
|
||||
present; /temperature/desired/0 is only written via the temperature_ocf
|
||||
kind, on boards that have the OCF pair)."""
|
||||
write = airconditioner.CLIMATE.entities[0].write_fn
|
||||
assert write(('power', True), {}) == (['power', '0'], {'value': True})
|
||||
assert write(('power', False), {}) == (['power', '0'], {'value': False})
|
||||
assert write(('power', True), {}) == (
|
||||
['power', 'vs', '0'], {'x.com.samsung.da.power': 'On'})
|
||||
assert write(('power', False), {}) == (
|
||||
['power', 'vs', '0'], {'x.com.samsung.da.power': 'Off'})
|
||||
assert write(('mode', 'Heat'), {}) == (
|
||||
['mode', 'vs', '0'], {'x.com.samsung.da.modes': ['Heat']})
|
||||
assert write(('temperature', 23.6), {}) == (
|
||||
# OCF-pair boards: temperature_ocf -> /temperature/desired/0.
|
||||
assert write(('temperature_ocf', 23.6), {}) == (
|
||||
['temperature', 'desired', '0'], {'temperature': 24})
|
||||
# Vendor boards: temperature -> /temperatures/vs/0, carrying only the id
|
||||
# and the changed field -- the device merges current/min/max/unit itself
|
||||
# (see common.merge_items_field, wired into async_send_command, for the
|
||||
# read-side half that keeps the optimistic cache complete).
|
||||
assert write(('temperature', 22), {}) == (
|
||||
['temperatures', 'vs', '0'],
|
||||
{'x.com.samsung.da.items': [
|
||||
{'x.com.samsung.da.id': '0', 'x.com.samsung.da.desired': '22'}]})
|
||||
assert write(('fan', '2'), {}) == (
|
||||
['wind', 'strength', 'vs', '0'], {'x.com.samsung.da.modes': '2'})
|
||||
assert write(('swing', 'All'), {}) == (
|
||||
@@ -114,11 +129,13 @@ def test_climate_write_targets():
|
||||
def test_climate_consumed_hrefs_declared_as_coverage():
|
||||
"""The climate-consumed and ambiguous hrefs are declared in the AC registry
|
||||
(as no-entity coverage caps) so they don't leak as gaps -- but produce no
|
||||
standalone entities."""
|
||||
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)."""
|
||||
reg, _ = _ac()
|
||||
for href in ('/power/0', '/power/vs/0', '/temperature/desired/0',
|
||||
'/wind/strength/vs/0', '/mode/convenient/vs/0',
|
||||
'/temperatures/vs/0', '/sensors/vs/0', '/humidity/0'):
|
||||
'/sensors/vs/0', '/humidity/0'):
|
||||
caps = reg.capabilities.get(href)
|
||||
assert caps, href
|
||||
assert all(c.entities == () for c in caps), href
|
||||
@@ -257,3 +274,134 @@ def test_current_limit_is_read_only():
|
||||
than a guessed writable control."""
|
||||
for desc in airconditioner.CURRENT_LIMIT.entities:
|
||||
assert getattr(desc, 'write_fn', None) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TP1X_DA-AC-RAC-01001 cool-only global variant (issue #91). Same modelNum as
|
||||
# the issue #38 board above, but its /otninformation/vs/0 ships no
|
||||
# swVersionInfo block, so oneUiVersion is empty and detection must fall back
|
||||
# to the hyphenated '-RAC-' modelNum token (the older '_RAC_' underscore match
|
||||
# doesn't fire on this DA-AC-RAC spelling). Adds /stepcontrol/vs/0 and
|
||||
# /remotedeviceinfo/vs/0 (both ignored) and exposes the WindFree preset via
|
||||
# the Nano/NanoSleep convenient-mode codes. Its panel light is carried inside
|
||||
# /mode/vs/0's options blob instead of a dedicated /light/vs/0 switch.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_tp1x_rac_coolonly_resolves_via_hyphenated_model_fallback():
|
||||
"""Empty oneUiVersion -> resolved by the '-RAC-' modelNum token, not
|
||||
for_device(). Guards the regression where this unit loaded as 'unknown'."""
|
||||
resources = _load_device('airconditioner_tp1x_rac_coolonly')
|
||||
otn = resources.get('/otninformation/vs/0', {})
|
||||
assert otn.get('swVersionInfo', {}).get('oneUiVersion', '') == ''
|
||||
reg, _ = _resolve('airconditioner_tp1x_rac_coolonly')
|
||||
assert reg is not None and reg.name == 'airconditioner'
|
||||
|
||||
|
||||
def test_tp1x_rac_coolonly_no_unbound_hrefs():
|
||||
"""Every resource binds or is ignored -- including the two hrefs unique
|
||||
to this dump (/stepcontrol/vs/0, /remotedeviceinfo/vs/0). Clears the gap
|
||||
repair."""
|
||||
reg, resources = _resolve('airconditioner_tp1x_rac_coolonly')
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_tp1x_rac_coolonly_stray_hrefs_ignored():
|
||||
ignored_hrefs = {cap.href for cap in airconditioner.COVERAGE}
|
||||
assert '/stepcontrol/vs/0' in ignored_hrefs
|
||||
assert '/remotedeviceinfo/vs/0' in ignored_hrefs
|
||||
|
||||
|
||||
def test_tp1x_rac_coolonly_climate_bound():
|
||||
reg, resources = _resolve('airconditioner_tp1x_rac_coolonly')
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||
climate = [b for b in bound if isinstance(b.desc, ClimateDesc)]
|
||||
assert len(climate) == 1 and climate[0].href == '/mode/vs/0'
|
||||
|
||||
|
||||
def test_tp1x_rac_coolonly_display_light_from_mode_options():
|
||||
"""This board has no /light/vs/0 switch; the panel light lives in
|
||||
/mode/vs/0's options and surfaces as a display_light switch. The token is
|
||||
inverted vs its name (confirmed by a live toggle test): with the panel
|
||||
lit the option reads `Light_Off`, and with it dark it reads `Light_On`."""
|
||||
reg, resources = _resolve('airconditioner_tp1x_rac_coolonly')
|
||||
state = flatten(discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
|
||||
assert state.get('display_light') is True
|
||||
|
||||
|
||||
def test_display_light_option_parsing_and_gating():
|
||||
# Inverted token: Light_Off -> panel lit (on), Light_On -> panel dark (off).
|
||||
lit = {'x.com.samsung.da.options': ['CoolCapa_35', 'Light_Off', 'Volume_Mute']}
|
||||
dark = {'x.com.samsung.da.options': ['Light_On']}
|
||||
absent = {'x.com.samsung.da.options': ['Volume_Mute']}
|
||||
assert airconditioner._display_light_on(lit) is True
|
||||
assert airconditioner._display_light_on(dark) is False
|
||||
assert airconditioner._display_light_on(absent) is None
|
||||
assert airconditioner._has_display_light_option(lit, {}) is True
|
||||
assert airconditioner._has_display_light_option(absent, {}) is False
|
||||
|
||||
|
||||
def test_mode_options_display_light_write_is_inverted_single_token():
|
||||
"""Turning the lamp ON writes the inverted 'Light_Off' token as a
|
||||
single-element options list (single-token merge); OFF writes 'Light_On'."""
|
||||
sw = next(e for e in airconditioner.CLIMATE.entities if e.key == 'display_light')
|
||||
assert sw.write_fn('On', {}) == (
|
||||
['mode', 'vs', '0'], {'x.com.samsung.da.options': ['Light_Off']})
|
||||
assert sw.write_fn('Off', {}) == (
|
||||
['mode', 'vs', '0'], {'x.com.samsung.da.options': ['Light_On']})
|
||||
|
||||
|
||||
def test_light_switch_board_gates_out_mode_options_light():
|
||||
"""Boards with a real /light/vs/0 switch carry no Light_* option, so the
|
||||
mode-options display-light entity doesn't double up (mutually exclusive
|
||||
encodings)."""
|
||||
reg, resources = _resolve('airconditioner_tp1x_rac')
|
||||
assert airconditioner._has_display_light_option(resources['/mode/vs/0'], resources) is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WindFree unit (issue #75): same ARTIK051_PRAC_20K modelNum family as the
|
||||
# original issue #17 fixture, but its /mode/convenient/vs/0 additionally
|
||||
# supports Nano/NanoSleep/MotionDirect/MotionIndirect, /wind/direction/vs/0
|
||||
# additionally supports Left_And_Right, and /humidity/vs/0's
|
||||
# fivepercentHumidity is populated (unlike the all-zero original dump).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _ac_windfree():
|
||||
resources = _load_device('airconditioner_windfree')
|
||||
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_windfree_no_unbound_hrefs():
|
||||
reg, resources = _ac_windfree()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_windfree_humidity_and_temperature_sensors_present():
|
||||
reg, resources = _ac_windfree()
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||
state = flatten(bound, resources)
|
||||
assert state['humidity'] == 42.0 # fivepercentHumidity, not the stuck humidity=0 field
|
||||
assert state['current_temperature_c'] == 27.0
|
||||
|
||||
|
||||
def test_current_temperature_vs_only_binds_when_ocf_href_absent():
|
||||
"""CURRENT_TEMPERATURE_VS's match_fn must not double-bind alongside
|
||||
CURRENT_TEMPERATURE when a device (like this one) reports both
|
||||
/temperature/current/0 and /temperatures/vs/0."""
|
||||
match = airconditioner.CURRENT_TEMPERATURE_VS.match_fn
|
||||
assert match({}, {'/temperature/current/0': {}}) is False
|
||||
assert match({}, {}) is True
|
||||
|
||||
|
||||
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
|
||||
|
||||
+248
-3
@@ -42,9 +42,12 @@ class TestForDevice:
|
||||
assert registry.name == 'refrigerator'
|
||||
|
||||
def test_for_device_returns_cooktop_registry(self):
|
||||
"""The registry's own .name is 'gas_cooktop' (disambiguated from
|
||||
induction_cooktop), but the lookup key devices route through stays
|
||||
'cooktop' -- oneUiVersion "Cooktop" still resolves here."""
|
||||
registry = for_device('7.0 Cooktop')
|
||||
assert registry is not None
|
||||
assert registry.name == 'cooktop'
|
||||
assert registry.name == 'gas_cooktop'
|
||||
|
||||
def test_for_device_returns_range_hood_registry(self):
|
||||
registry = for_device('7.0 Range Hood')
|
||||
@@ -111,6 +114,46 @@ class TestWasherRegistry:
|
||||
assert href in registry.capabilities, f"{href} missing from washer registry"
|
||||
|
||||
|
||||
class TestModelNumSegments:
|
||||
def test_splits_pipe_prefix_on_underscore(self):
|
||||
from custom_components.localthings.registry.by_type import _model_num_segments
|
||||
assert _model_num_segments('ARTIK051_DONGLE_REF|00127641|000800200014') == [
|
||||
'ARTIK051', 'DONGLE', 'REF',
|
||||
]
|
||||
|
||||
def test_ignores_everything_after_first_pipe(self):
|
||||
from custom_components.localthings.registry.by_type import _model_num_segments
|
||||
assert _model_num_segments('TP2X_RAC_20K|abc|REF_should_not_appear') == [
|
||||
'TP2X', 'RAC', '20K',
|
||||
]
|
||||
|
||||
def test_empty_for_none_or_empty_input(self):
|
||||
from custom_components.localthings.registry.by_type import _model_num_segments
|
||||
assert _model_num_segments('') == ['']
|
||||
assert _model_num_segments(None) == ['']
|
||||
|
||||
|
||||
class TestConsumerModelKey:
|
||||
def test_finds_key_in_last_segment(self):
|
||||
from custom_components.localthings.registry.by_type import _consumer_model_key
|
||||
assert _consumer_model_key('DA_WM_TP1_21_COMMON_WW5000C') == 'washer'
|
||||
|
||||
def test_finds_key_before_a_trailing_unrecognized_segment(self):
|
||||
"""Issue #79: 'DVE50A8800_8600' pairs two model numbers -- the real
|
||||
consumer token is the second-to-last segment, not the last."""
|
||||
from custom_components.localthings.registry.by_type import _consumer_model_key
|
||||
assert _consumer_model_key(
|
||||
'DA_WM_TP1_21_COMMON_DVE50A8800_8600/DC92-02835A_0080') == 'dryer'
|
||||
|
||||
def test_ignores_everything_after_first_slash(self):
|
||||
from custom_components.localthings.registry.by_type import _consumer_model_key
|
||||
assert _consumer_model_key('DA_WM_TP1_21_COMMON_WW5000C/DW9000_board') == 'washer'
|
||||
|
||||
def test_none_when_no_segment_matches(self):
|
||||
from custom_components.localthings.registry.by_type import _consumer_model_key
|
||||
assert _consumer_model_key('ARTIK051_DONGLE_REF') is None
|
||||
|
||||
|
||||
class TestForDeviceByModel:
|
||||
"""Fallback device-type detection for hardware without oneUiVersion."""
|
||||
|
||||
@@ -142,6 +185,20 @@ class TestForDeviceByModel:
|
||||
assert reg is not None
|
||||
assert reg.name == 'dryer'
|
||||
|
||||
def test_dryer_dve50a8600_paired_model_numbers_in_description(self):
|
||||
"""Issue #79: description pairs two model numbers
|
||||
('..._DVE50A8800_8600/DC92-...'), so the 'DV' consumer token is one
|
||||
segment before the literal last segment ('8600', which has no
|
||||
recognizable prefix on its own). The old last-segment-only check
|
||||
fell through to 'unknown' here."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'DA_WM_TP1_21_COMMON|20286441|300000010015110002A3031700000000',
|
||||
'DA_WM_TP1_21_COMMON_DVE50A8800_8600/DC92-02835A_0080',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'dryer'
|
||||
|
||||
def test_dishwasher_not_misdetected_as_washer(self):
|
||||
"""Dishwasher's modelNum contains the substring 'WW' -- must not
|
||||
be misrouted by a naive substring match."""
|
||||
@@ -178,6 +235,20 @@ class TestForDeviceByModel:
|
||||
assert reg is not None
|
||||
assert reg.name == 'refrigerator'
|
||||
|
||||
def test_refrigerator_dongle_ref_pipe_delimited_modelnum(self):
|
||||
"""Issues #77/#83: the ARTIK051_DONGLE_REF family's modelNum is
|
||||
'<board>_DONGLE_REF|<rest>' -- REF is the last underscore segment
|
||||
before the pipe, with no trailing underscore, so the plain '_REF_'
|
||||
substring check used to miss it entirely and the device fell back
|
||||
to 'unknown' with only common capabilities."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'ARTIK051_DONGLE_REF|00127641|00080020001430300100000000000000',
|
||||
'ARTIK_REF_17K',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'refrigerator'
|
||||
|
||||
def test_airconditioner_via_prac_token(self):
|
||||
"""Issue #17: a room AC (ARTIK051_PRAC_20K) reports no oneUiVersion and
|
||||
an unrecognized consumer token ('20K'); it falls back to the '_PRAC_'
|
||||
@@ -190,6 +261,104 @@ class TestForDeviceByModel:
|
||||
assert reg is not None
|
||||
assert reg.name == 'airconditioner'
|
||||
|
||||
def test_dehumidifier_via_dhm_token(self):
|
||||
"""Issue #88: a dehumidifier (AY18CG7500GED) shares the DA_AC_ board
|
||||
family with the room-AC models but reports no oneUiVersion and
|
||||
carries the '_DHM_' (DeHuMidifier) token instead of
|
||||
'_RAC_'/'_PRAC_'/'_WAC_'; falls back to the '_DHM_' token in
|
||||
modelNum."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'TP1X_DA_AC_DHM_01001_0000|10253841|77000000001700000A00000000000000',
|
||||
'TP1X_DA_AC_DHM_01001_0000',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'dehumidifier'
|
||||
|
||||
def test_water_purifier_via_waterpurifier_token(self):
|
||||
"""Issue #90: a water purifier (TP2X_WATERPURIFIER_20K) reports no
|
||||
oneUiVersion and no consumer-prefix match; falls back to the
|
||||
'WATERPURIFIER' token shared by modelNum and description."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'TP2X_WATERPURIFIER_20K|00132341|900000000215130001060F0000020000',
|
||||
'TP2X_WATERPURIFIER_20K',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'water_purifier'
|
||||
|
||||
def test_airconditioner_via_wac_token(self):
|
||||
"""Issue #87: a Bespoke Window AC (AW06C7155EWAZ) reports no
|
||||
oneUiVersion and a modelNum carrying the '_WAC_' (Window Air
|
||||
Conditioner) token instead of '_RAC_'/'_PRAC_'; falls back to the
|
||||
'_WAC_' token in modelNum."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'TP1X_DA_AC_WAC_01001_0000|40460041|50030018001611020A00000000000000',
|
||||
'AW06C7155EWAZ',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'airconditioner'
|
||||
|
||||
def test_wac_token_not_shadowed_by_wa_washer_prefix(self):
|
||||
"""Regression: adding the 'WA' consumer-model prefix (issue #106)
|
||||
must not hijack devices whose description literally equals their
|
||||
modelNum (e.g. the Window AC family, issue #87) and so contains a
|
||||
'WAC' segment -- 'WAC'[:2] == 'WA' would otherwise match the washer
|
||||
prefix before the more specific '_WAC_' modelNum check ever runs."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'TP1X_DA_AC_WAC_01001_0000|40460041|50030018001611020A00000000000000',
|
||||
'TP1X_DA_AC_WAC_01001_0000',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'airconditioner'
|
||||
|
||||
def test_airconditioner_via_ara_ww_token(self):
|
||||
"""Issues #115/#116/#117/#120: ARA-WW-TP1-22-COMMON wall-mount RACs
|
||||
(AR10/13/18BYEAAWKNME) report no oneUiVersion and no
|
||||
'_RAC_'/'-RAC-'/'_PRAC_' token at all -- falls back to the
|
||||
'ARA-WW-' token in modelNum, reusing the same airconditioner
|
||||
registry as every other TP1X-class room AC rather than a new
|
||||
device type."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'ARA-WW-TP1-22-COMMON|10229641|6001051A001511014600083200800000',
|
||||
'ARA-WW-TP1-22-COMMON',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'airconditioner'
|
||||
|
||||
def test_oven_via_microwave_token(self):
|
||||
"""Issue #121: a combi microwave (MW7300B-/EU1) reports no
|
||||
oneUiVersion and an unrecognized consumer token; falls back to the
|
||||
'-MICROWAVE-' token in modelNum onto the *existing* oven registry
|
||||
(same '/oven/vs/0' cavity + cook-mode shape), not a new device
|
||||
type."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'TP1X_DA-KS-MICROWAVE-01041|40475341|50040100021811000A00000000000000',
|
||||
'MW7300B-/EU1',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'oven'
|
||||
|
||||
def test_vacuum_station_via_vskr_token(self):
|
||||
"""Issue #131: a stick-vacuum clean/auto-empty station
|
||||
(A-VSKR-TP1-22-VS9500AL) reports no oneUiVersion and no
|
||||
washer/dryer/dishwasher consumer prefix; falls back to the
|
||||
'-VSKR-' token in modelNum onto a new vacuum_station registry (its
|
||||
dump has no vacuum-body state at all, only station-specific
|
||||
dustbag/dustbin/UV-sanitize resources -- see
|
||||
capabilities/vacuum_station.py's module docstring)."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'A-VSKR-TP1-22-VS9500AL|50023541|80030100001611000800000000000000',
|
||||
'A-VSKR-TP1-22-VS9500AL',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'vacuum_station'
|
||||
|
||||
def test_cooktop_via_legacy_model_description(self):
|
||||
"""Older cooktops identify themselves as ARTIK051_GLOBAL_COOKTOP."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
@@ -198,7 +367,7 @@ class TestForDeviceByModel:
|
||||
'ARTIK051_GLOBAL_COOKTOP',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'cooktop'
|
||||
assert reg.name == 'gas_cooktop'
|
||||
|
||||
def test_range_hood_via_ahd_model(self):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
@@ -229,6 +398,17 @@ class TestForDeviceByModel:
|
||||
assert reg is not None
|
||||
assert reg.name == 'washer'
|
||||
|
||||
def test_washer_wa_prefix(self):
|
||||
"""Top-load washers (e.g. WA8000T) use the WA consumer-model prefix
|
||||
-- issue #106."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'DA_WM_TP2_20_COMMON|20233741|200000010014110002A3020200000000',
|
||||
'DA_WM_TP2_20_COMMON_WA8000T/DC92-02810A_0002',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'washer'
|
||||
|
||||
def test_oven_via_oven_token(self):
|
||||
"""Issue #55: a wall oven (NV7000BS/ET5) reports no oneUiVersion and
|
||||
an unrecognized consumer token ('NV'); it falls back to the '-OVEN-'
|
||||
@@ -241,6 +421,28 @@ class TestForDeviceByModel:
|
||||
assert reg is not None
|
||||
assert reg.name == 'oven'
|
||||
|
||||
def test_induction_cooktop_via_hyphenated_cooktop_token(self):
|
||||
"""Issue #86: a standalone induction cooktop (NV8500T-/KO4) reports
|
||||
no oneUiVersion and an unrecognized consumer token ('NV'); it falls
|
||||
back to the hyphenated '-COOKTOP-' token in modelNum -- distinct
|
||||
from the underscore-delimited '_COOKTOP'/'_GB_CT_' check, which is
|
||||
the unrelated older NA9300K gas-cooktop family."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'TP1X_DA-KS-COOKTOP-01011|40459741|50000203001711000A00000000000000',
|
||||
'NV8500T-/KO4',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'induction_cooktop'
|
||||
|
||||
def test_hyphenated_cooktop_token_not_confused_with_range(self):
|
||||
"""'-COOKTOP-' and '-RANGE-' must route to distinct registries even
|
||||
though both are TP1X_DA-KS- board-family siblings."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model('TP1X_DA-KS-COOKTOP-01011', 'NV8500T-/KO4')
|
||||
assert reg is not None
|
||||
assert reg.name != 'range'
|
||||
|
||||
def test_unknown_model_returns_none(self):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model('SOME-UNKNOWN-BOARD', 'SOME-UNKNOWN-BOARD')
|
||||
@@ -259,7 +461,7 @@ class TestForDeviceByResources:
|
||||
reg = for_device_by_resources(_load_device('cooktop'))
|
||||
|
||||
assert reg is not None
|
||||
assert reg.name == 'cooktop'
|
||||
assert reg.name == 'gas_cooktop'
|
||||
|
||||
def test_unrelated_mode_options_are_not_cooktop(self):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_resources
|
||||
@@ -284,3 +486,46 @@ class TestForDeviceByResources:
|
||||
reg = for_device_by_resources(resources)
|
||||
assert reg is not None
|
||||
assert reg.name == 'range_hood'
|
||||
|
||||
def test_ne63b8411ss_without_information_or_burner_status_is_range(self):
|
||||
"""Issue #74: no oneUiVersion, no /information/vs/0 at all, and no
|
||||
/cooktop/status/vs/0 burner array -- only /cooktopmonitoring/vs/0.
|
||||
'Bake' in supportedModes plus that monitoring resource must still
|
||||
route this to the range registry, not plain oven or unknown."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_resources
|
||||
resources = {
|
||||
'/mode/vs/0': {
|
||||
'x.com.samsung.da.supportedModes': ['Bake', 'Broil', 'SelfClean'],
|
||||
'x.com.samsung.da.options': ['DeviceType_NE8411B-/AC0'],
|
||||
},
|
||||
'/oven/vs/0': {'x.com.samsung.da.state': 'Ready'},
|
||||
'/cooktopmonitoring/vs/0': {'x.com.samsung.da.cooktopRunningState': 'Ready'},
|
||||
}
|
||||
reg = for_device_by_resources(resources)
|
||||
assert reg is not None
|
||||
assert reg.name == 'range'
|
||||
|
||||
def test_bake_without_cooktop_resource_is_plain_oven(self):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_resources
|
||||
resources = {
|
||||
'/mode/vs/0': {
|
||||
'x.com.samsung.da.supportedModes': ['Bake', 'Broil'],
|
||||
'x.com.samsung.da.options': ['DeviceType_SOME_OVEN'],
|
||||
},
|
||||
'/oven/vs/0': {'x.com.samsung.da.state': 'Ready'},
|
||||
}
|
||||
reg = for_device_by_resources(resources)
|
||||
assert reg is not None
|
||||
assert reg.name == 'oven'
|
||||
|
||||
def test_bake_without_oven_cavity_resource_is_not_matched(self):
|
||||
"""'Bake' alone isn't enough -- the oven cavity resource must also
|
||||
be present, or this falls through to None like any other unknown
|
||||
shape."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_resources
|
||||
resources = {
|
||||
'/mode/vs/0': {
|
||||
'x.com.samsung.da.supportedModes': ['Bake', 'Broil'],
|
||||
},
|
||||
}
|
||||
assert for_device_by_resources(resources) is None
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Tests for the AC HVAC-mode/preset device<->HA maps in climate.py (issue #93).
|
||||
|
||||
Module-level dicts/constants with no coordinator/entity dependency, so --
|
||||
like `_temps_vs_item` in test_climate_temperature_fallback.py -- they're
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
def test_auto_maps_to_hvac_auto():
|
||||
"""The device's 'Auto' is a single-setpoint "device decides" mode -> HA
|
||||
HVACMode.AUTO, not HEAT_COOL (issue #91 review): HEAT_COOL implies a
|
||||
two-setpoint heat+cool range these single-setpoint units (including
|
||||
cool-only models) don't have. AIComfort is handled separately, not
|
||||
folded into this map."""
|
||||
assert _DEVICE_TO_HVAC['Auto'] == HVACMode.AUTO
|
||||
|
||||
|
||||
def test_aicomfort_not_in_flat_hvac_map():
|
||||
"""AIComfort isn't a flat _DEVICE_TO_HVAC entry -- it's an AI overlay on
|
||||
top of 'Auto', modeled as hvac_mode=AUTO + a dedicated preset instead of
|
||||
a distinct HVACMode value (see the climate.py module comment)."""
|
||||
assert _AI_COMFORT_MODE not in _DEVICE_TO_HVAC
|
||||
|
||||
|
||||
def test_hvac_auto_writes_back_to_plain_auto_not_aicomfort():
|
||||
"""HVACMode.AUTO is reachable via async_set_hvac_mode -- it writes the
|
||||
device's plain 'Auto' code. AIComfort stays reachable only through the
|
||||
ai_comfort preset, since it isn't a flat _DEVICE_TO_HVAC entry (see
|
||||
test_aicomfort_not_in_flat_hvac_map) and so can never win the reverse
|
||||
{v: k} dict even though both map to HVACMode.AUTO conceptually."""
|
||||
assert _HVAC_TO_DEVICE[HVACMode.AUTO] == 'Auto'
|
||||
|
||||
|
||||
def test_fan_only_still_reachable_via_wind():
|
||||
"""Guard against regressing the existing 'Wind' -> FAN_ONLY entry while
|
||||
editing this map."""
|
||||
assert _DEVICE_TO_HVAC['Wind'] == HVACMode.FAN_ONLY
|
||||
|
||||
|
||||
def test_fan_only_still_reachable_via_fan():
|
||||
"""'Fan' (e.g. TP1X_DA-AC-RAC-01011) is a second FAN_ONLY spelling
|
||||
alongside 'Wind' -- issue #91."""
|
||||
assert _DEVICE_TO_HVAC['Fan'] == HVACMode.FAN_ONLY
|
||||
|
||||
|
||||
def test_fan_only_reverse_fallback_prefers_wind():
|
||||
"""_device_code_for_hvac() resolves FAN_ONLY from a unit's own
|
||||
supportedModes first, so _HVAC_TO_DEVICE is only a fallback for a unit
|
||||
reporting no supportedModes at all. That fallback must stay 'Wind' (the
|
||||
original single spelling, predating 'Fan') rather than silently
|
||||
flipping to whichever of the two duplicate-value entries happens to
|
||||
come last in _DEVICE_TO_HVAC."""
|
||||
assert _HVAC_TO_DEVICE[HVACMode.FAN_ONLY] == 'Wind'
|
||||
|
||||
|
||||
def test_preset_ai_comfort_constant():
|
||||
assert PRESET_AI_COMFORT == 'ai_comfort'
|
||||
|
||||
|
||||
def test_preset_to_ha_off_maps_to_preset_none():
|
||||
from homeassistant.components.climate import PRESET_NONE
|
||||
assert _preset_to_ha('Off') == PRESET_NONE
|
||||
|
||||
|
||||
def test_preset_to_ha_lowercases_other_codes():
|
||||
"""Every other device code is exposed as its lowercased self -- resolved
|
||||
dynamically, not via a per-model table (issue #91)."""
|
||||
assert _preset_to_ha('Sleep') == 'sleep'
|
||||
assert _preset_to_ha('NanoSleep') == 'nanosleep'
|
||||
assert _preset_to_ha('MotionIndirect') == 'motionindirect'
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Tests for the /wind/oscillation/vs/0 swing fallback helper in climate.py
|
||||
(issue #126).
|
||||
|
||||
`_oscillation_swing` is a pure module-level function, like `_temps_vs_item`
|
||||
in test_climate_temperature_fallback.py -- testable directly without a
|
||||
coordinator/entity. This covers the fallback-selection logic that the
|
||||
registry-level "no unbound hrefs" test in test_golden_regression.py doesn't
|
||||
reach: newer WindFree firmware reports no /wind/direction/vs/0 at all, so
|
||||
swing_mode/swing_modes would silently be None/empty without this fallback.
|
||||
"""
|
||||
from custom_components.localthings.climate import _oscillation_swing
|
||||
|
||||
|
||||
def test_oscillation_swing_off_when_both_fixed():
|
||||
assert _oscillation_swing({'vertical': 'Fix', 'horizontal': 'Fix'}) == 'off'
|
||||
|
||||
|
||||
def test_oscillation_swing_vertical_only():
|
||||
assert _oscillation_swing({'vertical': 'Swing', 'horizontal': 'Fix'}) == 'vertical'
|
||||
|
||||
|
||||
def test_oscillation_swing_horizontal_only():
|
||||
assert _oscillation_swing({'vertical': 'Fix', 'horizontal': 'Swing'}) == 'horizontal'
|
||||
|
||||
|
||||
def test_oscillation_swing_both():
|
||||
assert _oscillation_swing({'vertical': 'Swing', 'horizontal': 'Swing'}) == 'both'
|
||||
|
||||
|
||||
def test_oscillation_swing_none_when_resource_absent():
|
||||
"""Boards with /wind/direction/vs/0 instead don't populate this
|
||||
resource at all -- callers must fall through cleanly, not raise."""
|
||||
assert _oscillation_swing({}) is None
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Tests for the horizontal-swing mapping added to climate.py for issue #75
|
||||
-- pure dict lookups, testable without a coordinator/entity fixture (see
|
||||
test_climate_temperature_fallback.py).
|
||||
|
||||
The preset side of issue #75 (WindFree/motion convenient modes not
|
||||
surfacing) is intentionally not addressed here: climate.py's
|
||||
_preset_to_ha() (issue #91) reads any device preset code straight off the
|
||||
unit's own supportedModes, replacing the old static _DEVICE_TO_PRESET
|
||||
table with a generic resolver that covers WindFree/motion generically
|
||||
instead of a per-model dict -- duplicating that here would just conflict
|
||||
with it.
|
||||
"""
|
||||
from custom_components.localthings.climate import _DEVICE_TO_SWING, _SWING_TO_DEVICE
|
||||
|
||||
|
||||
def test_horizontal_swing_maps_both_directions():
|
||||
assert _DEVICE_TO_SWING['Left_And_Right'] == 'horizontal'
|
||||
assert _SWING_TO_DEVICE['horizontal'] == 'Left_And_Right'
|
||||
|
||||
|
||||
def test_existing_swing_modes_unchanged():
|
||||
assert _DEVICE_TO_SWING['Fix'] == 'off'
|
||||
assert _DEVICE_TO_SWING['All'] == 'both'
|
||||
assert _DEVICE_TO_SWING['Up_And_Low'] == 'vertical'
|
||||
@@ -1,5 +1,7 @@
|
||||
from custom_components.localthings.registry.capabilities import common
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.entities import BinarySensorDesc, SwitchDesc
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _reg():
|
||||
@@ -57,6 +59,42 @@ class TestMergeOptionsField:
|
||||
assert common.merge_options_field(cached, ['nounderscore']) == ['Course_16']
|
||||
|
||||
|
||||
class TestMergeItemsField:
|
||||
"""merge_items_field() is the items[]-array counterpart of
|
||||
merge_options_field above (issue #91 review feedback): a vendor
|
||||
x.com.samsung.da.items[] write only needs to carry the item id plus the
|
||||
field(s) being changed, so the coordinator uses this to keep its
|
||||
optimistic cache entry complete (current/minimum/maximum/unit still
|
||||
present) without waiting on a real poll."""
|
||||
|
||||
def test_merges_fields_into_matching_id(self):
|
||||
cached = [{'x.com.samsung.da.id': '0', 'x.com.samsung.da.current': '20.0',
|
||||
'x.com.samsung.da.maximum': '30', 'x.com.samsung.da.minimum': '16'}]
|
||||
merged = common.merge_items_field(
|
||||
cached, [{'x.com.samsung.da.id': '0', 'x.com.samsung.da.desired': '22'}])
|
||||
assert merged == [{'x.com.samsung.da.id': '0', 'x.com.samsung.da.current': '20.0',
|
||||
'x.com.samsung.da.maximum': '30', 'x.com.samsung.da.minimum': '16',
|
||||
'x.com.samsung.da.desired': '22'}]
|
||||
|
||||
def test_appends_when_id_absent(self):
|
||||
cached = [{'x.com.samsung.da.id': '0', 'x.com.samsung.da.current': '20.0'}]
|
||||
merged = common.merge_items_field(
|
||||
cached, [{'x.com.samsung.da.id': '1', 'x.com.samsung.da.desired': '22'}])
|
||||
assert merged == [
|
||||
{'x.com.samsung.da.id': '0', 'x.com.samsung.da.current': '20.0'},
|
||||
{'x.com.samsung.da.id': '1', 'x.com.samsung.da.desired': '22'},
|
||||
]
|
||||
|
||||
def test_handles_missing_cache(self):
|
||||
assert common.merge_items_field(
|
||||
None, [{'x.com.samsung.da.id': '0', 'x.com.samsung.da.desired': '22'}]
|
||||
) == [{'x.com.samsung.da.id': '0', 'x.com.samsung.da.desired': '22'}]
|
||||
|
||||
def test_ignores_malformed_new_items(self):
|
||||
cached = [{'x.com.samsung.da.id': '0', 'x.com.samsung.da.current': '20.0'}]
|
||||
assert common.merge_items_field(cached, ['not-a-dict']) == cached
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OCF-native / vendor '-vs' fallback pairs (power, kids-lock, remote control).
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -65,7 +103,7 @@ class TestMergeOptionsField:
|
||||
class TestPowerFallback:
|
||||
def test_generic_href_read_write(self):
|
||||
assert common.POWER_GENERIC.href == '/power/0'
|
||||
desc = common.POWER_GENERIC.entities[0]
|
||||
desc = next(e for e in common.POWER_GENERIC.entities if isinstance(e, SwitchDesc))
|
||||
assert desc.value_fn(True) is True
|
||||
assert desc.value_fn(False) is False
|
||||
path, body = desc.write_fn('On', {})
|
||||
@@ -80,13 +118,72 @@ class TestPowerFallback:
|
||||
{}, {'/power/0': {}, '/power/vs/0': {}}) is False
|
||||
|
||||
def test_vs_fallback_read_write(self):
|
||||
desc = common.POWER_VS_FALLBACK.entities[0]
|
||||
desc = next(e for e in common.POWER_VS_FALLBACK.entities if isinstance(e, SwitchDesc))
|
||||
assert desc.value_fn('On') is True
|
||||
assert desc.value_fn('Off') is False
|
||||
path, body = desc.write_fn('On', {})
|
||||
assert path == ['power', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.power': 'On'}
|
||||
|
||||
def test_power_switch_hidden_when_firmware_disallows(self):
|
||||
switch = next(e for e in common.POWER_GENERIC.entities if isinstance(e, SwitchDesc))
|
||||
sensor = next(e for e in common.POWER_GENERIC.entities if isinstance(e, BinarySensorDesc))
|
||||
resources = {
|
||||
'/power/0': {'value': True},
|
||||
'/wm/setinfo/vs/0': {'x.com.samsung.da.isModelSettingPowerOnOff': 'false'},
|
||||
}
|
||||
assert switch.exists_fn(resources['/power/0'], resources) is False
|
||||
assert sensor.exists_fn(resources['/power/0'], resources) is True
|
||||
|
||||
def test_power_switch_writable_when_firmware_allows(self):
|
||||
switch = next(e for e in common.POWER_GENERIC.entities if isinstance(e, SwitchDesc))
|
||||
sensor = next(e for e in common.POWER_GENERIC.entities if isinstance(e, BinarySensorDesc))
|
||||
resources = {
|
||||
'/power/0': {'value': True},
|
||||
'/wm/setinfo/vs/0': {'x.com.samsung.da.isModelSettingPowerOnOff': 'true'},
|
||||
}
|
||||
assert switch.exists_fn(resources['/power/0'], resources) is True
|
||||
assert sensor.exists_fn(resources['/power/0'], resources) is False
|
||||
|
||||
def test_power_switch_default_writable_without_setinfo(self):
|
||||
switch = next(e for e in common.POWER_GENERIC.entities if isinstance(e, SwitchDesc))
|
||||
sensor = next(e for e in common.POWER_GENERIC.entities if isinstance(e, BinarySensorDesc))
|
||||
resources = {'/power/0': {'value': True}}
|
||||
assert switch.exists_fn(resources['/power/0'], resources) is True
|
||||
assert sensor.exists_fn(resources['/power/0'], resources) is False
|
||||
|
||||
|
||||
class TestWmSetinfoFlags:
|
||||
def test_washer_fixture_carries_setinfo_from_device0(self):
|
||||
"""/wm/setinfo/vs/0 lands in the seed snapshot -- no dedicated capability."""
|
||||
resources = _load_device('washer')
|
||||
assert '/wm/setinfo/vs/0' in resources
|
||||
assert common.model_allows_power_on_off(resources) is False
|
||||
assert common.model_setting_without_sc(resources) is True
|
||||
|
||||
def test_dishwasher_allows_power_on_off(self):
|
||||
resources = _load_device('dishwasher')
|
||||
assert common.model_allows_power_on_off(resources) is True
|
||||
assert common.model_setting_without_sc(resources) is False
|
||||
|
||||
def test_deleted_alarms_filtered(self):
|
||||
assert common._active_alarm_codes([
|
||||
{
|
||||
'x.com.samsung.da.code': 'ErrorCode',
|
||||
'x.com.samsung.da.state': 'Deleted',
|
||||
},
|
||||
]) == 'none'
|
||||
assert common._active_alarm_codes([
|
||||
{
|
||||
'x.com.samsung.da.code': 'LE',
|
||||
'x.com.samsung.da.state': 'Triggered',
|
||||
},
|
||||
{
|
||||
'x.com.samsung.da.code': 'ErrorCode',
|
||||
'x.com.samsung.da.state': 'Deleted',
|
||||
},
|
||||
]) == 'LE'
|
||||
|
||||
|
||||
class TestKidsLockFallback:
|
||||
def test_generic_read_write(self):
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Tests for Samsung dehumidifier support (issue #88, AY18CG7500GED).
|
||||
|
||||
HA-free like the rest of the suite: exercises the registry, discovery/
|
||||
flatten, and the target-humidity/operating-mode write contracts.
|
||||
"""
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.entities import NumberDesc, SelectDesc
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _dehumidifier():
|
||||
resources = _load_device('dehumidifier')
|
||||
info = resources['/information/vs/0']
|
||||
reg = for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'],
|
||||
)
|
||||
return reg, resources
|
||||
|
||||
|
||||
def _bound():
|
||||
reg, resources = _dehumidifier()
|
||||
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
|
||||
|
||||
|
||||
def _state():
|
||||
bound, resources = _bound()
|
||||
return flatten(bound, resources)
|
||||
|
||||
|
||||
def _desc(key):
|
||||
bound, _ = _bound()
|
||||
return next(b.desc for b in bound if b.desc.key == key)
|
||||
|
||||
|
||||
def test_model_resolves_to_dehumidifier_registry():
|
||||
reg, _ = _dehumidifier()
|
||||
assert reg is not None and reg.name == 'dehumidifier'
|
||||
|
||||
|
||||
def test_no_unbound_hrefs():
|
||||
"""Every resource in the issue #88 dump binds or is covered -- clears
|
||||
the coverage-gap repair."""
|
||||
reg, resources = _dehumidifier()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_expected_state_keys_present():
|
||||
state = _state()
|
||||
for key in ('humidity', 'target_humidity', 'operating_mode', 'power_switch',
|
||||
'auto_clean', 'air_filter_status', 'mute_once'):
|
||||
assert key in state, key
|
||||
|
||||
|
||||
def test_humidity_sensor_reads_current_value():
|
||||
state = _state()
|
||||
assert state['humidity'] == 47
|
||||
|
||||
|
||||
def test_target_humidity_number_reads_desired_value():
|
||||
state = _state()
|
||||
assert state['target_humidity'] == 50
|
||||
|
||||
|
||||
def test_target_humidity_write_contract():
|
||||
desc = _desc('target_humidity')
|
||||
assert isinstance(desc, NumberDesc)
|
||||
path, body = desc.write_fn('55', {})
|
||||
assert path == ['humidity', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.desiredHumidity': '55'}
|
||||
|
||||
|
||||
def test_target_humidity_step_reads_live_increment():
|
||||
"""Step comes from the device's own `increment` field rather than a
|
||||
hardcoded constant -- see the adding-device-support skill's 'never
|
||||
hard-code the one dump's values' section."""
|
||||
desc = _desc('target_humidity')
|
||||
assert desc.step_fn({'increment': '5'}) == 5
|
||||
assert desc.step_fn({'increment': '10'}) == 10
|
||||
# No live field: falls back to a sane default rather than raising.
|
||||
assert desc.step_fn({}) == 1
|
||||
|
||||
|
||||
def test_target_humidity_has_no_hardcoded_bounds():
|
||||
"""No range field is present in any dump seen so far -- native_min/max
|
||||
are deliberately left unset (falls back to HA's own 0-100 default for a
|
||||
percentage field) rather than a bound guessed from a spec sheet."""
|
||||
desc = _desc('target_humidity')
|
||||
assert desc.native_min is None
|
||||
assert desc.native_max is None
|
||||
assert desc.native_min_fn is None
|
||||
assert desc.native_max_fn is None
|
||||
|
||||
|
||||
def test_operating_mode_select_options_come_from_live_supported_modes():
|
||||
"""Options are read live from x.com.samsung.da.supportedModes, not a
|
||||
hardcoded tuple -- so a future device with a different mode set is
|
||||
handled automatically."""
|
||||
desc = _desc('operating_mode')
|
||||
assert isinstance(desc, SelectDesc)
|
||||
assert desc.options_field == 'x.com.samsung.da.supportedModes'
|
||||
assert desc.options == ()
|
||||
|
||||
|
||||
def test_operating_mode_write_contract():
|
||||
desc = _desc('operating_mode')
|
||||
path, body = desc.write_fn('Quiet', {})
|
||||
assert path == ['mode', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.modes': ['Quiet']}
|
||||
|
||||
|
||||
def test_operating_mode_reads_first_mode():
|
||||
state = _state()
|
||||
assert state['operating_mode'] == 'Smart'
|
||||
@@ -20,9 +20,32 @@ class TestTempCurrentGeneric:
|
||||
assert desc.unit_fn({'temperature': 5.0}) == '°F'
|
||||
|
||||
|
||||
class TestDoorGeneric:
|
||||
"""Issues #77/#83: DOOR_GENERIC used to read only the bare `openState`
|
||||
field. Most /door/* resources use that, but the ARTIK051_DONGLE_REF
|
||||
family's /door/onedoorfreezer/vs/0 reports the vendor-prefixed
|
||||
`x.com.samsung.da.openState` instead -- the pattern capability still
|
||||
bound the href either way, but the entity's value was always None."""
|
||||
|
||||
def test_reads_bare_open_state(self):
|
||||
desc = fridge.DOOR_GENERIC.entities[0]
|
||||
assert desc.rep_fn({'openState': 'Open'}) is True
|
||||
assert desc.rep_fn({'openState': 'Close'}) is False
|
||||
|
||||
def test_reads_vendor_prefixed_open_state(self):
|
||||
desc = fridge.DOOR_GENERIC.entities[0]
|
||||
assert desc.rep_fn({'x.com.samsung.da.openState': 'Open'}) is True
|
||||
assert desc.rep_fn({'x.com.samsung.da.openState': 'Close'}) is False
|
||||
|
||||
def test_prefers_bare_field_when_both_present(self):
|
||||
desc = fridge.DOOR_GENERIC.entities[0]
|
||||
rep = {'openState': 'Open', 'x.com.samsung.da.openState': 'Close'}
|
||||
assert desc.rep_fn(rep) is True
|
||||
|
||||
|
||||
class TestTempSetpointGeneric:
|
||||
def test_unit_reads_celsius(self):
|
||||
desc = fridge.TEMP_SETPOINT_GENERIC.entities[0]
|
||||
desc = fridge.TEMP_SETPOINT.entities[0]
|
||||
assert desc.unit_fn({'temperature': -19.0, 'units': 'C'}) == '°C'
|
||||
|
||||
|
||||
@@ -231,6 +254,75 @@ class TestArtik051AndTp2xFixturesHaveCompleteCoverage:
|
||||
assert state['flex_zone_mode'] == 'CV_FDR_BEVERAGE'
|
||||
|
||||
|
||||
class TestArtik051DongleRefFixtureCoverage:
|
||||
"""Issues #77/#83: the ARTIK051_DONGLE_REF standalone freezer resolves
|
||||
to the refrigerator registry (segment-based modelNum detection) with
|
||||
zero unbound hrefs, and its door/temperature entities -- which only
|
||||
bind through pattern capabilities the 'unknown' fallback never tried --
|
||||
now carry real values."""
|
||||
|
||||
def test_no_unbound_hrefs_and_expected_entities(self):
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import (
|
||||
for_device_by_model, refrigerator,
|
||||
)
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from tests.conftest import _load_device
|
||||
|
||||
resources = _load_device('refrigerator_artik051_dongle_ref')
|
||||
info = resources['/information/vs/0']
|
||||
reg = for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
|
||||
assert reg is not None and reg.name == 'refrigerator'
|
||||
|
||||
unbound = []
|
||||
bound = discover(
|
||||
resources, refrigerator.REGISTRY.capabilities,
|
||||
refrigerator.REGISTRY.pattern_capabilities, log=unbound.append,
|
||||
)
|
||||
assert unbound == []
|
||||
state = flatten(bound, resources)
|
||||
assert state['door_onedoorfreezer_open'] is False # rep reports 'Close'
|
||||
assert state['freezer_temperature'] == -20.0
|
||||
assert state['freezer_setpoint'] == -20.0
|
||||
|
||||
|
||||
class TestArtik051DongleRefCoolerFixtureCoverage:
|
||||
"""Issue #78: RR40M7165WW, the fridge half of the same household
|
||||
ARTIK051_DONGLE_REF dongle setup as issue #77's freezer. Notably reports
|
||||
*two* door hrefs (/door/cooler/0 and /door/onedoorfreezer/vs/0) despite
|
||||
being a single-door fridge, not a fridge/freezer combo -- apparently
|
||||
shared firmware naming across the product line, not a real second
|
||||
compartment. Both must still resolve to zero unbound hrefs and real
|
||||
values with the same fix as #77."""
|
||||
|
||||
def test_no_unbound_hrefs_and_expected_entities(self):
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import (
|
||||
for_device_by_model, refrigerator,
|
||||
)
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from tests.conftest import _load_device
|
||||
|
||||
resources = _load_device('refrigerator_artik051_dongle_ref_cooler')
|
||||
info = resources['/information/vs/0']
|
||||
reg = for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'])
|
||||
assert reg is not None and reg.name == 'refrigerator'
|
||||
|
||||
unbound = []
|
||||
bound = discover(
|
||||
resources, refrigerator.REGISTRY.capabilities,
|
||||
refrigerator.REGISTRY.pattern_capabilities, log=unbound.append,
|
||||
)
|
||||
assert unbound == []
|
||||
state = flatten(bound, resources)
|
||||
assert state['door_cooler_open'] is False
|
||||
assert state['door_onedoorfreezer_open'] is False
|
||||
assert state['cooler_temperature'] == 3.0
|
||||
assert state['cooler_setpoint'] == 3.0
|
||||
|
||||
|
||||
class TestRefrigeratorAiEnergyLevelFixtureCoverage:
|
||||
"""AI energy-saving level (common.AI_ENERGY_LEVEL, see
|
||||
test_common_capabilities.py) is exercised end-to-end here against the
|
||||
|
||||
@@ -61,6 +61,21 @@ def test_registry_reproduces_golden_state_keys_for_washer():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_washer_wa8000t():
|
||||
"""Top-load washer (WA8000T, issue #106) reports no oneUiVersion and
|
||||
used the 'WA' consumer-model prefix, previously unmapped in
|
||||
_CONSUMER_PREFIX_TO_KEY -- fell back to 'unknown'."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('washer_wa8000t')
|
||||
golden = json.loads((GOLDEN / 'washer_wa8000t.json').read_text())
|
||||
state_keys = _new_state_keys('washer_wa8000t', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_dryer():
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('dryer')
|
||||
@@ -73,6 +88,23 @@ def test_registry_reproduces_golden_state_keys_for_dryer():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_dryer_dve50a8600():
|
||||
"""DVE50A8600V/A3 (issue #79) -- description pairs two model numbers
|
||||
('..._DVE50A8800_8600/...'), so the true 'DV' consumer-model token sits
|
||||
one segment before the actual last segment ('8600'). The old
|
||||
last-segment-only check missed it and fell back to 'unknown'; resolved
|
||||
via _consumer_model_key scanning segments from the end."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('dryer_dve50a8600')
|
||||
golden = json.loads((GOLDEN / 'dryer_dve50a8600.json').read_text())
|
||||
state_keys = _new_state_keys('dryer_dve50a8600', 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():
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('airconditioner')
|
||||
@@ -85,6 +117,54 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_dehumidifier():
|
||||
"""TP1X_DA_AC_DHM_01001_0000 (issue #88, AY18CG7500GED) shares the DA_AC_
|
||||
board family with the room-AC models but carries the '_DHM_' token;
|
||||
resolved via the '_DHM_' modelNum fallback in for_device_by_model into a
|
||||
dedicated dehumidifier registry (target humidity, operating mode, reused
|
||||
AC filter/auto-clean/mute-once capabilities)."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('dehumidifier')
|
||||
golden = json.loads((GOLDEN / 'dehumidifier.json').read_text())
|
||||
state_keys = _new_state_keys('dehumidifier', 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_water_purifier():
|
||||
"""TP2X_WATERPURIFIER_20K (issue #90) reports no oneUiVersion; resolved
|
||||
via the 'WATERPURIFIER' modelNum/description fallback into a dedicated
|
||||
water_purifier registry (dispense settings, sterilize/filter status,
|
||||
favorite capacity, and the three lock switches)."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('water_purifier')
|
||||
golden = json.loads((GOLDEN / 'water_purifier.json').read_text())
|
||||
state_keys = _new_state_keys('water_purifier', 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_water_purifier_coffee():
|
||||
"""TP2X_WATERPURIFIER_20K coffee-capable variant (issue #107) adds
|
||||
/favorite/coffee/vs/0, /favorite/hotwater/vs/0, and three static
|
||||
coffee-recipe resources not present in issue #90's original dump."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('water_purifier_coffee')
|
||||
golden = json.loads((GOLDEN / 'water_purifier_coffee.json').read_text())
|
||||
state_keys = _new_state_keys('water_purifier_coffee', 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_cooktop():
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('cooktop')
|
||||
@@ -166,6 +246,45 @@ def test_registry_reproduces_golden_state_keys_for_artik051_ref_17k():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_artik051_dongle_ref_cooler():
|
||||
"""RR40M7165WW (issue #78) -- the same ARTIK051_DONGLE_REF household
|
||||
dongle family as issue #77's freezer, but the fridge half: reports
|
||||
/door/cooler/0 *and* /door/onedoorfreezer/vs/0 (the latter apparently
|
||||
shared firmware naming, not an actual second freezer compartment) plus
|
||||
/temperature/{current,desired}/cooler/0. Same pipe-delimited modelNum
|
||||
detection gap and DOOR_GENERIC field-name gap as #77, same fix."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('refrigerator_artik051_dongle_ref_cooler')
|
||||
golden = json.loads((GOLDEN / 'refrigerator_artik051_dongle_ref_cooler.json').read_text())
|
||||
state_keys = _new_state_keys('refrigerator_artik051_dongle_ref_cooler', 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_artik051_dongle_ref():
|
||||
"""ARTIK051_DONGLE_REF standalone freezer (issues #77/#83) -- reports no
|
||||
oneUiVersion and a pipe-delimited modelNum ('..._DONGLE_REF|<rest>')
|
||||
that the old '_REF_' substring check missed entirely; resolved via the
|
||||
segment-based check in for_device_by_model. Its door
|
||||
(/door/onedoorfreezer/vs/0) and temperature
|
||||
(/temperature/{current,desired}/freezer/0) hrefs only bind through
|
||||
fridge.py's pattern capabilities, which the 'unknown' fallback never
|
||||
tries -- so this also regression-tests that those resources bind at
|
||||
all once routed to the right registry."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('refrigerator_artik051_dongle_ref')
|
||||
golden = json.loads((GOLDEN / 'refrigerator_artik051_dongle_ref.json').read_text())
|
||||
state_keys = _new_state_keys('refrigerator_artik051_dongle_ref', 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_tp2x_ref_20k():
|
||||
"""TP2X_REF_20K -- CV_FDR_-prefixed flex zone (issue #32) plus the extra
|
||||
energy fields (cumulativeConsumption/monthlyConsumption/
|
||||
@@ -231,6 +350,23 @@ def test_registry_reproduces_golden_state_keys_for_caww_tp2():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_window_ac():
|
||||
"""TP1X_DA_AC_WAC_01001_0000 (issue #87, Bespoke Window AC AW06C7155EWAZ)
|
||||
reports no oneUiVersion and carries the '_WAC_' (Window Air Conditioner)
|
||||
token instead of '_RAC_'/'_PRAC_'; resolved via the '_WAC_' modelNum
|
||||
fallback in for_device_by_model. Otherwise binds cleanly against the
|
||||
existing airconditioner registry with zero unbound hrefs."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('airconditioner_window_ac')
|
||||
golden = json.loads((GOLDEN / 'airconditioner_window_ac.json').read_text())
|
||||
state_keys = _new_state_keys('airconditioner_window_ac', 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_tp1x_rac():
|
||||
"""TP1X_DA-AC-RAC-01001_0000 (issue #38) -- fuller RAC board with display
|
||||
light, self-check, mute-once, and a current-limit setting."""
|
||||
@@ -245,6 +381,38 @@ def test_registry_reproduces_golden_state_keys_for_tp1x_rac():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_tp1x_rac_coolonly():
|
||||
"""TP1X_DA-AC-RAC-01001 cool-only global variant (issue #91) whose
|
||||
/otninformation/vs/0 ships no swVersionInfo block -- resolves via the
|
||||
hyphenated '-RAC-' modelNum fallback rather than for_device()."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('airconditioner_tp1x_rac_coolonly')
|
||||
golden = json.loads((GOLDEN / 'airconditioner_tp1x_rac_coolonly.json').read_text())
|
||||
state_keys = _new_state_keys('airconditioner_tp1x_rac_coolonly', 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_windfree():
|
||||
"""ARTIK051_PRAC_20K, WindFree-capable unit (issue #75) -- same modelNum
|
||||
family as the original issue #17 fixture, but its /mode/convenient/vs/0
|
||||
additionally reports Nano/NanoSleep/MotionDirect/MotionIndirect, its
|
||||
/wind/direction/vs/0 reports Left_And_Right, and /humidity/vs/0's
|
||||
fivepercentHumidity is actually populated."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('airconditioner_windfree')
|
||||
golden = json.loads((GOLDEN / 'airconditioner_windfree.json').read_text())
|
||||
state_keys = _new_state_keys('airconditioner_windfree', 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():
|
||||
"""Range/cooktop-oven combo (model TP1X_DA-KS-RANGE-0102X, issue #44) --
|
||||
reports no oneUiVersion; resolved via the '-RANGE-' modelNum token
|
||||
@@ -262,6 +430,37 @@ def test_registry_reproduces_golden_state_keys_for_range():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_induction_cooktop():
|
||||
"""Standalone induction cooktop, no oven attached (model
|
||||
TP1X_DA-KS-COOKTOP-01011, issue #86) -- reports no oneUiVersion and a
|
||||
hyphenated '-COOKTOP-' modelNum token, resolved via
|
||||
for_device_by_model into its own 'induction_cooktop' registry (not
|
||||
cooktop.REGISTRY, which is the unrelated NA9300K gas-cooktop family)."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('induction_cooktop')
|
||||
golden = json.loads((GOLDEN / 'induction_cooktop.json').read_text())
|
||||
state_keys = _new_state_keys('induction_cooktop', resources)
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_range_no_info():
|
||||
"""NE63B8411SS (issue #74) -- reports no oneUiVersion *and* no
|
||||
/information/vs/0 at all, so neither for_device nor
|
||||
for_device_by_model has anything to key off; resolved via the 'Bake'-
|
||||
in-supportedModes + /cooktopmonitoring/vs/0 signature in
|
||||
for_device_by_resources. This board's local API has no per-burner
|
||||
/cooktop/status/vs/0 array either -- only the coarse
|
||||
/cooktopmonitoring/vs/0 monitoring resource covered by range.py's
|
||||
COOKTOP_MONITORING."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('range_no_info')
|
||||
golden = json.loads((GOLDEN / 'range_no_info.json').read_text())
|
||||
state_keys = _new_state_keys('range_no_info', 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():
|
||||
"""ARTIK051_TVTL_18K (issue #56) -- reports no oneUiVersion; resolved via
|
||||
the '_TVTL_' modelNum token fallback in for_device_by_model."""
|
||||
@@ -294,6 +493,142 @@ def test_registry_reproduces_golden_state_keys_for_oven():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_washer_wa55a7700av():
|
||||
"""DA_WM_TP1_21_COMMON top-load washer (model WA55A7700AV, issue #111)
|
||||
-- a different board generation than the WA8000T's TP2_20_COMMON,
|
||||
reached through the same 'WA' consumer-model-prefix fallback (issue
|
||||
#106). Binds cleanly against the existing washer registry with zero
|
||||
unbound hrefs."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('washer_wa55a7700av')
|
||||
golden = json.loads((GOLDEN / 'washer_wa55a7700av.json').read_text())
|
||||
state_keys = _new_state_keys('washer_wa55a7700av', 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_ne8300d():
|
||||
"""TP1X_DA-KS-RANGE-0102X (model NE8300D, issue #112) -- reports no
|
||||
oneUiVersion; resolved via the '-RANGE-' modelNum token fallback.
|
||||
Binds cleanly against the existing range registry, including
|
||||
/cooktopmonitoring/vs/0 via range.COOKTOP_MONITORING, with zero
|
||||
unbound hrefs."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('range_ne8300d')
|
||||
golden = json.loads((GOLDEN / 'range_ne8300d.json').read_text())
|
||||
state_keys = _new_state_keys('range_ne8300d', 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
|
||||
'_RAC_'/'-RAC-' token -- resolved via the 'ARA-WW-' modelNum fallback.
|
||||
Same resource surface as the other TP1X-class room ACs; binds cleanly
|
||||
against the existing airconditioner registry with zero unbound hrefs,
|
||||
so no new device type was needed."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('airconditioner_ara_ww_tp1_22')
|
||||
golden = json.loads((GOLDEN / 'airconditioner_ara_ww_tp1_22.json').read_text())
|
||||
state_keys = _new_state_keys('airconditioner_ara_ww_tp1_22', 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_windfree_oscillation():
|
||||
"""TP1X_DA-AC-RAC-01011_0000 Bespoke AI WindFree Deluxe (model
|
||||
AR60H10D1JWNME, issue #126) -- the same board family as the
|
||||
airconditioner_tp1x_da_ac_rac_01011 fixture, but a newer firmware
|
||||
variant reporting no /wind/direction/vs/0 at all: swing lives on the
|
||||
2-axis /wind/oscillation/vs/0 resource instead (airconditioner.
|
||||
HREF_WIND_OSCILLATION, climate.py's oscillation fallback), and it adds
|
||||
an /anomalyload/vs/0 overload-response resource (airconditioner.
|
||||
ANOMALY_LOAD, read-only per the 'don't guess' rule). Binds cleanly
|
||||
with zero unbound hrefs."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('airconditioner_windfree_oscillation')
|
||||
golden = json.loads(
|
||||
(GOLDEN / 'airconditioner_windfree_oscillation.json').read_text()
|
||||
)
|
||||
state_keys = _new_state_keys('airconditioner_windfree_oscillation', 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_oven_mw7300b():
|
||||
"""TP1X_DA-KS-MICROWAVE-01041 combi microwave (model MW7300B, issue
|
||||
#121) -- reports no oneUiVersion; resolved via the '-MICROWAVE-'
|
||||
modelNum token fallback onto the *existing* oven registry rather than
|
||||
a new device type, since it shares the same '/oven/vs/0' cavity and
|
||||
'/mode/vs/0' cook-mode shape (Convection/AirFryer/Grill/MicroWave*).
|
||||
The only href the oven registry didn't already cover was
|
||||
/recipe/cook/vs/0 (oven.OVEN_RECIPE_COOK, an empty quick-recipe-display
|
||||
blob with no entity)."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('oven_mw7300b')
|
||||
golden = json.loads((GOLDEN / 'oven_mw7300b.json').read_text())
|
||||
state_keys = _new_state_keys('oven_mw7300b', 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
|
||||
air_purifier registry (shared with the older ARTIK051_TVTL family via
|
||||
per-href match_fn discrimination -- see capabilities/air_purifier.py).
|
||||
Its /mode/vs/0 reports modes/supportedModes directly (Smart/Max/Mid/
|
||||
WindFree/Sleep) rather than the older family's packed options[] scheme,
|
||||
which the new air_purifier.FAN capability now binds to a real `fan`
|
||||
entity (PRESET_MODE, not an ordered speed -- see fan.py). Binds with
|
||||
zero unbound hrefs."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('air_purifier_tp1x_da_ac_air')
|
||||
golden = json.loads((GOLDEN / 'air_purifier_tp1x_da_ac_air.json').read_text())
|
||||
state_keys = _new_state_keys('air_purifier_tp1x_da_ac_air', 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_vacuum_station():
|
||||
"""A-VSKR-TP1-22-VS9500AL stick-vacuum clean/auto-empty station (issue
|
||||
#131) -- reports no oneUiVersion; resolved via the new '-VSKR-'
|
||||
modelNum fallback onto a new vacuum_station registry (see
|
||||
capabilities/vacuum_station.py's module docstring for why this needed
|
||||
a new device type rather than reusing an existing one: the dump has no
|
||||
vacuum-body state at all, only station-specific dustbag/dustbin/
|
||||
UV-sanitize resources that share no hrefs with anything else already
|
||||
modeled). Binds with zero unbound hrefs."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('vacuum_station')
|
||||
golden = json.loads((GOLDEN / 'vacuum_station.json').read_text())
|
||||
state_keys = _new_state_keys('vacuum_station', 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 = {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Tests for the standalone induction-cooktop registry (issue #86)."""
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model, induction_cooktop
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _cooktop():
|
||||
resources = _load_device('induction_cooktop')
|
||||
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 = _cooktop()
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||
return flatten(bound, resources)
|
||||
|
||||
|
||||
def test_model_resolves_to_induction_cooktop_registry():
|
||||
reg, _ = _cooktop()
|
||||
assert reg is not None and reg.name == 'induction_cooktop'
|
||||
assert reg is induction_cooktop.REGISTRY
|
||||
|
||||
|
||||
def test_no_unbound_hrefs():
|
||||
"""Every resource in the issue #86 dump binds or is ignored -- clears
|
||||
the coverage-gap repair."""
|
||||
reg, resources = _cooktop()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_expected_entities_present():
|
||||
state = _state()
|
||||
for key in (
|
||||
'cooktop_state', 'cooktop_power', 'cooktop_child_lock',
|
||||
'burner_0_power_level', 'burner_0_state', 'burner_0_hot_surface',
|
||||
'burner_0_pan_detected', 'burner_2_pan_detected',
|
||||
'paired_hood_connected', 'probe_connected', 'probe_temperature',
|
||||
'energy_kwh', 'alarm_code',
|
||||
):
|
||||
assert key in state, key
|
||||
|
||||
|
||||
def test_unreported_burners_gated_out():
|
||||
"""Dump reports numberOfBurners=3 (indices 0-2) -- burner slot 3+ of
|
||||
range.MAX_BURNERS must not appear as entities."""
|
||||
state = _state()
|
||||
assert 'burner_3_power_level' not in state
|
||||
|
||||
|
||||
def test_recipe_status_href_is_ignored_not_unbound():
|
||||
"""/cooktop/recipe/status/vs/0 is idle/empty on this dump -- covered by
|
||||
ignored.py, not modeled as an entity (same treatment as the microwave
|
||||
family's /recipe/cook/vs/0)."""
|
||||
reg, _ = _cooktop()
|
||||
ignored_hrefs = {cap.href for caps in reg.capabilities.values() for cap in caps
|
||||
if cap.entities == ()}
|
||||
assert '/cooktop/recipe/status/vs/0' in ignored_hrefs
|
||||
@@ -82,3 +82,41 @@ def test_burner_hot_surface_true_when_not_normal():
|
||||
if e.key == 'burner_0_hot_surface')
|
||||
assert desc.value_fn([{'burnerNumber': 0, 'hotSurfaceState': 'hot'}]) is True
|
||||
assert desc.value_fn([{'burnerNumber': 0, 'hotSurfaceState': 'normal'}]) is False
|
||||
|
||||
|
||||
def test_burner_pan_detected(): # issue #86
|
||||
desc = next(e for e in range_caps.COOKTOP_STATUS.entities
|
||||
if e.key == 'burner_0_pan_detected')
|
||||
assert desc.value_fn([{'burnerNumber': 0, 'panDetection': True}]) is True
|
||||
assert desc.value_fn([{'burnerNumber': 0, 'panDetection': False}]) is False
|
||||
assert desc.value_fn([{'burnerNumber': 1, 'panDetection': True}]) is False # different burner
|
||||
|
||||
|
||||
def test_cooktop_power_is_read_only(): # issue #86
|
||||
desc = next(e for e in range_caps.COOKTOP_STATUS.entities if e.key == 'cooktop_power')
|
||||
assert desc.value_fn('On') is True
|
||||
assert desc.value_fn('Off') is False
|
||||
assert not hasattr(desc, 'write_fn') # BinarySensorDesc has no write path at all
|
||||
|
||||
|
||||
def test_cooktop_child_lock_write(): # issue #86
|
||||
desc = next(e for e in range_caps.COOKTOP_STATUS.entities if e.key == 'cooktop_child_lock')
|
||||
assert desc.value_fn('on') is True
|
||||
assert desc.value_fn('off') is False
|
||||
path, body = desc.write_fn('On', {})
|
||||
assert path == ['cooktop', 'status', 'vs', '0']
|
||||
assert body == {'childLock': 'on'}
|
||||
assert desc.write_fn('Bogus', {}) is None
|
||||
|
||||
|
||||
def test_probe_status_temperature_unit_reads_live_field(): # issue #86
|
||||
desc = next(e for e in range_caps.PROBE_STATUS.entities if e.key == 'probe_temperature')
|
||||
assert desc.unit_fn({'temperatureUnit': 'C'}) == '°C'
|
||||
assert desc.unit_fn({'temperatureUnit': 'F'}) == '°F'
|
||||
assert desc.unit_fn({}) == '°C'
|
||||
|
||||
|
||||
def test_probe_status_connected_value_fn(): # issue #86
|
||||
desc = next(e for e in range_caps.PROBE_STATUS.entities if e.key == 'probe_connected')
|
||||
assert desc.value_fn('Connected') is True
|
||||
assert desc.value_fn('Disconnected') is False
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Tests for the no-/information/vs/0, no-burner-status range variant
|
||||
(NE63B8411SS-class, issue #74)."""
|
||||
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 range as range_caps
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _range():
|
||||
resources = _load_device('range_no_info')
|
||||
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():
|
||||
"""Every resource in the issue #74 dump binds or is ignored -- clears
|
||||
the coverage-gap repair."""
|
||||
reg, resources = _range()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_no_burner_entities():
|
||||
"""This board reports no /cooktop/status/vs/0, so none of range.py's
|
||||
per-burner entities should appear -- only COOKTOP_MONITORING's."""
|
||||
state = _state()
|
||||
assert not any(k.startswith('burner_') for k in state)
|
||||
|
||||
|
||||
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',
|
||||
'cooktop_running_state', 'warming_center_state',
|
||||
):
|
||||
assert key in state, key
|
||||
|
||||
|
||||
def test_cooktop_monitoring_reads_live_fields():
|
||||
desc = next(e for e in range_caps.COOKTOP_MONITORING.entities if e.key == 'cooktop_running_state')
|
||||
assert desc.value_fn('Ready') == 'Ready'
|
||||
desc = next(e for e in range_caps.COOKTOP_MONITORING.entities if e.key == 'warming_center_state')
|
||||
assert desc.value_fn('Off') == 'Off'
|
||||
@@ -168,3 +168,43 @@ def test_all_entity_state_translation_keys_are_lowercase():
|
||||
for translation in platform.values():
|
||||
for state_key in translation.get("state", {}):
|
||||
assert state_key == state_key.lower()
|
||||
|
||||
|
||||
def test_every_ac_convenient_mode_code_has_a_preset_label():
|
||||
"""issue #91 review feedback #3: AC preset resolution is fully dynamic
|
||||
(climate._preset_to_ha), so every fixture's /mode/convenient/vs/0
|
||||
supportedModes code surfaces as a preset -- an unlabelled one falls back
|
||||
to its raw device code in the UI. Cheap guard against repeating that gap:
|
||||
every non-'Off' code across every AC fixture must either resolve to one
|
||||
of HA's own auto-localized standard presets or have an explicit label in
|
||||
en.json.
|
||||
"""
|
||||
from homeassistant.components.climate.const import (
|
||||
PRESET_ACTIVITY, PRESET_AWAY, PRESET_BOOST, PRESET_COMFORT,
|
||||
PRESET_ECO, PRESET_HOME, PRESET_SLEEP,
|
||||
)
|
||||
standard = {PRESET_ACTIVITY, PRESET_AWAY, PRESET_BOOST, PRESET_COMFORT,
|
||||
PRESET_ECO, PRESET_HOME, PRESET_SLEEP}
|
||||
preset_labels = set(
|
||||
_load("en")["entity"]["climate"]["airconditioner"]["state_attributes"]
|
||||
["preset_mode"]["state"]
|
||||
)
|
||||
fixtures_dir = Path(__file__).parent / "fixtures"
|
||||
missing = []
|
||||
for path in sorted(fixtures_dir.glob("airconditioner*_device.json")):
|
||||
dump = json.loads(path.read_text())
|
||||
conv = next(
|
||||
(item for item in dump.get("device0", [])
|
||||
if item.get("href") == "/mode/convenient/vs/0"), None,
|
||||
)
|
||||
if not conv:
|
||||
continue
|
||||
for code in conv["rep"].get("x.com.samsung.da.supportedModes", []):
|
||||
if code == "Off":
|
||||
continue
|
||||
label = code.lower()
|
||||
if label in standard or label in preset_labels:
|
||||
continue
|
||||
missing.append((path.name, code))
|
||||
assert missing == []
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
"""Tests for Samsung water-purifier support (issue #90, TP2X_WATERPURIFIER_20K).
|
||||
|
||||
HA-free like the rest of the suite: exercises the registry, discovery/
|
||||
flatten, and the write contracts.
|
||||
"""
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.entities import NumberDesc
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _water_purifier():
|
||||
resources = _load_device('water_purifier')
|
||||
info = resources['/information/vs/0']
|
||||
reg = for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'],
|
||||
)
|
||||
return reg, resources
|
||||
|
||||
|
||||
def _bound():
|
||||
reg, resources = _water_purifier()
|
||||
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
|
||||
|
||||
|
||||
def _state():
|
||||
bound, resources = _bound()
|
||||
return flatten(bound, resources)
|
||||
|
||||
|
||||
def _desc(key):
|
||||
bound, _ = _bound()
|
||||
return next(b.desc for b in bound if b.desc.key == key)
|
||||
|
||||
|
||||
def test_model_resolves_to_water_purifier_registry():
|
||||
reg, _ = _water_purifier()
|
||||
assert reg is not None and reg.name == 'water_purifier'
|
||||
|
||||
|
||||
def test_no_unbound_hrefs():
|
||||
"""Every resource in the issue #90 dump binds or is covered -- clears
|
||||
the coverage-gap repair."""
|
||||
reg, resources = _water_purifier()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_expected_state_keys_present():
|
||||
state = _state()
|
||||
for key in ('dispense_type', 'hot_water_temperature', 'dispense_capacity',
|
||||
'pouring', 'waterpurifier_status', 'filter_usage', 'filter_status',
|
||||
'hotwater_lock', 'coldwater_lock', 'buzz_lock'):
|
||||
assert key in state, key
|
||||
|
||||
|
||||
def test_dispense_type_options_come_from_live_supported_types():
|
||||
desc = _desc('dispense_type')
|
||||
assert desc.options_field == 'x.com.samsung.da.supportedTypes'
|
||||
|
||||
|
||||
def test_dispense_type_write_contract():
|
||||
desc = _desc('dispense_type')
|
||||
path, body = desc.write_fn('hotwater', {})
|
||||
assert path == ['setting', 'waterpurifier', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.desiredType': 'hotwater'}
|
||||
|
||||
|
||||
def test_hot_water_temperature_is_a_select_not_a_number():
|
||||
"""Only a handful of discrete temperatures are selectable (not a
|
||||
continuous range) -- confirmed by supportedHotTemperatures being a short
|
||||
enumerated list, not a [min, max] range field."""
|
||||
desc = _desc('hot_water_temperature')
|
||||
assert desc.options_field == 'x.com.samsung.da.supportedHotTemperatures'
|
||||
|
||||
|
||||
def test_dispense_capacity_bounds_come_live_not_hardcoded():
|
||||
"""Bounds and step come from the device's own desiredCapacityRange/
|
||||
capacityResolution fields, not a hardcoded constant -- see the
|
||||
adding-device-support skill's 'never hard-code the one dump's values'
|
||||
section."""
|
||||
desc = _desc('dispense_capacity')
|
||||
assert isinstance(desc, NumberDesc)
|
||||
assert desc.native_min is None
|
||||
assert desc.native_max is None
|
||||
assert desc.range_field == 'x.com.samsung.da.desiredCapacityRange'
|
||||
rep = {'x.com.samsung.da.desiredCapacityRange': ['50', '2000'],
|
||||
'x.com.samsung.da.capacityResolution': '10'}
|
||||
assert desc.step_fn(rep) == 10
|
||||
|
||||
|
||||
def test_dispense_capacity_write_contract():
|
||||
desc = _desc('dispense_capacity')
|
||||
path, body = desc.write_fn('550', {})
|
||||
assert path == ['setting', 'waterpurifier', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.desiredCapacity': '550'}
|
||||
|
||||
|
||||
def test_lock_switches_read_unlocked_as_off():
|
||||
state = _state()
|
||||
assert state['hotwater_lock'] is False
|
||||
assert state['coldwater_lock'] is False
|
||||
assert state['buzz_lock'] is False
|
||||
|
||||
|
||||
def test_lock_switch_write_contracts():
|
||||
hot = _desc('hotwater_lock')
|
||||
cold = _desc('coldwater_lock')
|
||||
buzz = _desc('buzz_lock')
|
||||
assert hot.write_fn('On', {}) == (
|
||||
['status', 'lock', 'vs', '0'], {'x.com.samsung.da.hotwaterLock': 'Locked'})
|
||||
assert cold.write_fn('Off', {}) == (
|
||||
['status', 'lock', 'vs', '0'], {'x.com.samsung.da.coldwaterLock': 'Unlocked'})
|
||||
assert buzz.write_fn('On', {}) == (
|
||||
['status', 'lock', 'vs', '0'], {'x.com.samsung.da.buzzLock': 'Locked'})
|
||||
|
||||
|
||||
def test_favorite_capacity_options_come_from_live_capacity_list():
|
||||
desc = _desc('favorite_capacity')
|
||||
assert desc.options_field == 'x.com.samsung.da.capacityList'
|
||||
|
||||
|
||||
def test_sterilize_timestamps_parsed_as_utc():
|
||||
state = _state()
|
||||
assert state['sterilize_last_time'].tzinfo is not None
|
||||
assert state['sterilize_plan_time'].tzinfo is not None
|
||||
|
||||
|
||||
def test_mode_hrefs_are_ignored_not_guessed():
|
||||
"""/mode/vs/0's supportedModes carries a single opaque wizard token and
|
||||
modes reports an unrelated value not even in supportedModes -- internal
|
||||
plumbing, left unmodeled per the 'don't guess' rule rather than exposed
|
||||
as a nonsensical select."""
|
||||
from custom_components.localthings.registry.capabilities import water_purifier
|
||||
ignored_hrefs = {cap.href for cap in water_purifier.COVERAGE}
|
||||
assert '/mode/vs/0' in ignored_hrefs
|
||||
assert '/automation/waterpurifier/vs/0' in ignored_hrefs
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Coffee-capable variant (issue #107) -- /favorite/coffee/vs/0 and
|
||||
# /favorite/hotwater/vs/0, not present in issue #90's original dump.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _water_purifier_coffee():
|
||||
resources = _load_device('water_purifier_coffee')
|
||||
info = resources['/information/vs/0']
|
||||
reg = for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'],
|
||||
)
|
||||
return reg, resources
|
||||
|
||||
|
||||
def _bound_coffee():
|
||||
reg, resources = _water_purifier_coffee()
|
||||
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
|
||||
|
||||
|
||||
def _state_coffee():
|
||||
bound, resources = _bound_coffee()
|
||||
return flatten(bound, resources)
|
||||
|
||||
|
||||
def _desc_coffee(key):
|
||||
bound, _ = _bound_coffee()
|
||||
return next(b.desc for b in bound if b.desc.key == key)
|
||||
|
||||
|
||||
def test_coffee_variant_no_unbound_hrefs():
|
||||
"""Every resource in the issue #107 dump binds or is covered, including
|
||||
the four coffee-recipe hrefs not present in issue #90's dump."""
|
||||
reg, resources = _water_purifier_coffee()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_coffee_variant_expected_state_keys_present():
|
||||
state = _state_coffee()
|
||||
for key in ('favorite_coffee_enabled', 'coffee_brew_status',
|
||||
'favorite_hotwater_enabled', 'favorite_hotwater_temperature'):
|
||||
assert key in state, key
|
||||
|
||||
|
||||
def test_favorite_coffee_write_contract():
|
||||
desc = _desc_coffee('favorite_coffee_enabled')
|
||||
assert desc.write_fn('On', {}) == (
|
||||
['favorite', 'coffee', 'vs', '0'], {'favorite.activate': 'On'})
|
||||
assert desc.write_fn('Off', {}) == (
|
||||
['favorite', 'coffee', 'vs', '0'], {'favorite.activate': 'Off'})
|
||||
|
||||
|
||||
def test_favorite_hotwater_write_contract():
|
||||
enabled = _desc_coffee('favorite_hotwater_enabled')
|
||||
assert enabled.write_fn('On', {}) == (
|
||||
['favorite', 'hotwater', 'vs', '0'], {'x.com.samsung.da.switchHotwater': 'Unlocked'})
|
||||
assert enabled.write_fn('Off', {}) == (
|
||||
['favorite', 'hotwater', 'vs', '0'], {'x.com.samsung.da.switchHotwater': 'Locked'})
|
||||
|
||||
|
||||
def test_favorite_hotwater_temperature_options_come_from_live_supported_list():
|
||||
desc = _desc_coffee('favorite_hotwater_temperature')
|
||||
assert desc.options_field == 'x.com.samsung.da.favorite.supportedList'
|
||||
|
||||
|
||||
def test_coffee_recipe_hrefs_are_ignored_not_guessed():
|
||||
"""Static capability-advertisement blobs or empty resources -- no live
|
||||
'current recipe'/'current custom slot' field to expose, per the 'don't
|
||||
guess' rule."""
|
||||
from custom_components.localthings.registry.capabilities import water_purifier
|
||||
ignored_hrefs = {cap.href for cap in water_purifier.COVERAGE}
|
||||
for href in ('/brand/recipe/info/vs/0', '/coffee/custom/recipe/vs/0',
|
||||
'/recipe/coffee/vs/0', '/recipe/coffee/deletion/vs/0'):
|
||||
assert href in ignored_hrefs, href
|
||||
Reference in New Issue
Block a user