Merge branch 'main' into claude/device-support-issue-90-water-purifier
This commit is contained in:
39 files changed
+2590
-33
No files matched your search
@@ -20,6 +20,8 @@ back for, power, mode, temperature and wind resources alike.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
@@ -53,6 +55,8 @@ from .const import DOMAIN
|
|||||||
from .coordinator import LocalThingsCoordinator
|
from .coordinator import LocalThingsCoordinator
|
||||||
from .entity import LocalThingsEntity, _is_included
|
from .entity import LocalThingsEntity, _is_included
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
_MODES_FIELD = 'x.com.samsung.da.modes'
|
_MODES_FIELD = 'x.com.samsung.da.modes'
|
||||||
_SUPPORTED_FIELD = 'x.com.samsung.da.supportedModes'
|
_SUPPORTED_FIELD = 'x.com.samsung.da.supportedModes'
|
||||||
|
|
||||||
@@ -68,6 +72,19 @@ _DEVICE_TO_HVAC: dict[str, HVACMode] = {
|
|||||||
}
|
}
|
||||||
_HVAC_TO_DEVICE = {v: k for k, v in _DEVICE_TO_HVAC.items()}
|
_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 plain 'Auto'
|
||||||
|
# would if it were ever mapped there) 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
|
# Fan (wind strength): device codes "0".."4" -> HA standard fan constants where
|
||||||
# a clean match exists so they auto-localize; "turbo" is custom (translated).
|
# a clean match exists so they auto-localize; "turbo" is custom (translated).
|
||||||
_DEVICE_TO_FAN: dict[str, str] = {
|
_DEVICE_TO_FAN: dict[str, str] = {
|
||||||
@@ -84,6 +101,7 @@ _DEVICE_TO_SWING: dict[str, str] = {
|
|||||||
'Fix': 'off',
|
'Fix': 'off',
|
||||||
'All': 'both',
|
'All': 'both',
|
||||||
'Up_And_Low': 'vertical',
|
'Up_And_Low': 'vertical',
|
||||||
|
'Left_And_Right': 'horizontal', # issue #75
|
||||||
}
|
}
|
||||||
_SWING_TO_DEVICE = {v: k for k, v in _DEVICE_TO_SWING.items()}
|
_SWING_TO_DEVICE = {v: k for k, v in _DEVICE_TO_SWING.items()}
|
||||||
|
|
||||||
@@ -164,6 +182,11 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
|||||||
| ClimateEntityFeature.TURN_ON
|
| ClimateEntityFeature.TURN_ON
|
||||||
| ClimateEntityFeature.TURN_OFF
|
| 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 ---------------------------------------------------
|
# -- resource helpers ---------------------------------------------------
|
||||||
|
|
||||||
@@ -180,13 +203,34 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
|||||||
def _supported(self, href: str) -> list[str]:
|
def _supported(self, href: str) -> list[str]:
|
||||||
return list(self._rep(href).get(_SUPPORTED_FIELD) or [])
|
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):
|
def _read_mode(self, href: str, mapping: dict):
|
||||||
"""Current mode of a wind/convenient resource, mapped to its HA value."""
|
"""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]:
|
def _read_modes(self, href: str, mapping: dict) -> list[str]:
|
||||||
"""Supported modes of a resource, mapped to HA values (unknowns dropped)."""
|
"""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 --------------------------------------------------------
|
# -- temperature --------------------------------------------------------
|
||||||
|
|
||||||
@@ -250,14 +294,23 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
|||||||
if not self._is_on():
|
if not self._is_on():
|
||||||
return HVACMode.OFF
|
return HVACMode.OFF
|
||||||
device = _first(self._rep(MODE_HREF).get(_MODES_FIELD))
|
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)
|
return _DEVICE_TO_HVAC.get(device, HVACMode.AUTO)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_modes(self) -> list[HVACMode]:
|
def hvac_modes(self) -> list[HVACMode]:
|
||||||
modes = [HVACMode.OFF]
|
modes = [HVACMode.OFF]
|
||||||
for m in self._supported(MODE_HREF):
|
for m in self._supported(MODE_HREF):
|
||||||
|
if m == _AI_COMFORT_MODE:
|
||||||
|
continue
|
||||||
mapped = _DEVICE_TO_HVAC.get(m)
|
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)
|
modes.append(mapped)
|
||||||
return modes
|
return modes
|
||||||
|
|
||||||
@@ -281,11 +334,16 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def preset_mode(self):
|
def preset_mode(self):
|
||||||
|
if _first(self._rep(MODE_HREF).get(_MODES_FIELD)) == _AI_COMFORT_MODE:
|
||||||
|
return PRESET_AI_COMFORT
|
||||||
return self._read_mode(CONVENIENT_HREF, _DEVICE_TO_PRESET)
|
return self._read_mode(CONVENIENT_HREF, _DEVICE_TO_PRESET)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def preset_modes(self) -> list[str]:
|
def preset_modes(self) -> list[str]:
|
||||||
return self._read_modes(CONVENIENT_HREF, _DEVICE_TO_PRESET)
|
modes = self._read_modes(CONVENIENT_HREF, _DEVICE_TO_PRESET)
|
||||||
|
if _AI_COMFORT_MODE in self._supported(MODE_HREF):
|
||||||
|
modes.append(PRESET_AI_COMFORT)
|
||||||
|
return modes
|
||||||
|
|
||||||
# -- writes -------------------------------------------------------------
|
# -- writes -------------------------------------------------------------
|
||||||
|
|
||||||
@@ -324,4 +382,10 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
|||||||
await self._set_mapped('swing', _SWING_TO_DEVICE, swing_mode)
|
await self._set_mapped('swing', _SWING_TO_DEVICE, swing_mode)
|
||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
|
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
|
||||||
await self._set_mapped('preset', _PRESET_TO_DEVICE, preset_mode)
|
await self._set_mapped('preset', _PRESET_TO_DEVICE, preset_mode)
|
||||||
@@ -197,6 +197,18 @@ def _find_live_ports(host: str, ports: list[int], timeout: float) -> list[int]:
|
|||||||
return _order_candidates(live)
|
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:
|
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."""
|
"""Fetch UUID, mint leaf cert, probe each port. Returns config entry data dict."""
|
||||||
import cbor2
|
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('/information/vs/0', {})
|
||||||
.get('x.com.samsung.da.serialNum', '')
|
.get('x.com.samsung.da.serialNum', '')
|
||||||
)
|
)
|
||||||
if not serial:
|
if not serial or _is_placeholder_serial(serial):
|
||||||
serial = f"{host}:{port}"
|
serial = f"{host}:{port}"
|
||||||
one_ui_version = (
|
one_ui_version = (
|
||||||
resources
|
resources
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ from smartthings_local.ocf.state_cache import StateCache
|
|||||||
|
|
||||||
from .registry.batch import parse_device0_batch
|
from .registry.batch import parse_device0_batch
|
||||||
from .registry.by_type import for_device, for_device_by_model, for_device_by_resources
|
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_options_field,
|
||||||
|
remote_control_enabled,
|
||||||
|
remote_control_required_for_write,
|
||||||
|
)
|
||||||
from .registry.discovery import discover, BoundEntity
|
from .registry.discovery import discover, BoundEntity
|
||||||
from .registry import CAPABILITIES
|
from .registry import CAPABILITIES
|
||||||
from .registry.adapter import flatten
|
from .registry.adapter import flatten
|
||||||
@@ -49,6 +53,22 @@ class _NoOpDescriptor:
|
|||||||
_RECOVERY_RETRY_S = 600.0 # re-attempt observe mode this often while polling
|
_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]]):
|
class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
"""Manages one Samsung appliance: session, discovery, polling."""
|
"""Manages one Samsung appliance: session, discovery, polling."""
|
||||||
|
|
||||||
@@ -312,7 +332,7 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
self._unbound_hrefs = unbound
|
self._unbound_hrefs = unbound
|
||||||
|
|
||||||
serial = info.get('x.com.samsung.da.serialNum', '')
|
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]
|
serial = self._entry.data[CONF_HOST]
|
||||||
self.device_serial = serial
|
self.device_serial = serial
|
||||||
|
|
||||||
@@ -531,7 +551,10 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
opted this device out of it via CONF_BYPASS_REMOTE_CONTROL (issue
|
opted this device out of it via CONF_BYPASS_REMOTE_CONTROL (issue
|
||||||
#54: some devices accept certain writes, e.g. a washer's default
|
#54: some devices accept certain writes, e.g. a washer's default
|
||||||
dosing levels, even while reporting remote control off, so the
|
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
|
desc = bound_entity.desc
|
||||||
write_fn = getattr(desc, 'write_fn', None)
|
write_fn = getattr(desc, 'write_fn', None)
|
||||||
if write_fn is None:
|
if write_fn is None:
|
||||||
@@ -540,7 +563,11 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
rep = self._cache.get(href or '') or {}
|
rep = self._cache.get(href or '') or {}
|
||||||
resources = self._cache.snapshot()
|
resources = self._cache.snapshot()
|
||||||
bypass_remote_control = self._entry.options.get(CONF_BYPASS_REMOTE_CONTROL, False)
|
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(
|
raise ServiceValidationError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="remote_control_disabled",
|
translation_key="remote_control_disabled",
|
||||||
@@ -553,7 +580,10 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key=error,
|
translation_key=error,
|
||||||
)
|
)
|
||||||
result = write_fn(payload, rep, href)
|
try:
|
||||||
|
result = write_fn(payload, rep, href, resources)
|
||||||
|
except TypeError:
|
||||||
|
result = write_fn(payload, rep, href)
|
||||||
if result is None:
|
if result is None:
|
||||||
self._log.warning("write_fn rejected payload %r for %s", payload, href)
|
self._log.warning("write_fn rejected payload %r for %s", payload, href)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -88,6 +88,26 @@ _CONSUMER_PREFIX_TO_KEY: dict[str, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
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]:
|
def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegistry]:
|
||||||
"""Fallback device-type detection for hardware that never reports
|
"""Fallback device-type detection for hardware that never reports
|
||||||
oneUiVersion (confirmed for washers -- their /otninformation/vs/0 has
|
oneUiVersion (confirmed for washers -- their /otninformation/vs/0 has
|
||||||
@@ -101,8 +121,7 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
|||||||
DeviceRegistry if the consumer-model code or modelNum resolves to a
|
DeviceRegistry if the consumer-model code or modelNum resolves to a
|
||||||
known type, None otherwise.
|
known type, None otherwise.
|
||||||
"""
|
"""
|
||||||
token = (description or '').split('/', 1)[0].rsplit('_', 1)[-1]
|
key = _consumer_model_key(description)
|
||||||
key = _CONSUMER_PREFIX_TO_KEY.get(token[:2].upper())
|
|
||||||
if key is None and '_REF_' in (model_num or ''):
|
if key is None and '_REF_' in (model_num or ''):
|
||||||
key = 'refrigerator'
|
key = 'refrigerator'
|
||||||
# Room air conditioners (e.g. ARTIK051_PRAC_20K) report no oneUiVersion and
|
# Room air conditioners (e.g. ARTIK051_PRAC_20K) report no oneUiVersion and
|
||||||
@@ -123,6 +142,15 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
|||||||
# resource (see airconditioner.py's _AC_IGNORED).
|
# resource (see airconditioner.py's _AC_IGNORED).
|
||||||
if key is None and '-CAWW-' in (model_num or '').upper():
|
if key is None and '-CAWW-' in (model_num or '').upper():
|
||||||
key = 'airconditioner'
|
key = 'airconditioner'
|
||||||
|
# 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'
|
||||||
# Air purifiers (e.g. ARTIK051_TVTL_18K, issue #56) report no
|
# Air purifiers (e.g. ARTIK051_TVTL_18K, issue #56) report no
|
||||||
# oneUiVersion either, and carry the '_TVTL_' board-family token.
|
# oneUiVersion either, and carry the '_TVTL_' board-family token.
|
||||||
if key is None and '_TVTL_' in (model_num or ''):
|
if key is None and '_TVTL_' in (model_num or ''):
|
||||||
@@ -175,4 +203,18 @@ def for_device_by_resources(resources: dict[str, dict]) -> Optional[DeviceRegist
|
|||||||
and '/hood/lamp/vs/0' in resources
|
and '/hood/lamp/vs/0' in resources
|
||||||
):
|
):
|
||||||
return _REGISTRY_BY_KEY['range_hood']
|
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
|
return None
|
||||||
@@ -25,6 +25,9 @@ REGISTRY = DeviceRegistry(
|
|||||||
airconditioner.DISPLAY_LIGHT,
|
airconditioner.DISPLAY_LIGHT,
|
||||||
airconditioner.MUTE_ONCE,
|
airconditioner.MUTE_ONCE,
|
||||||
airconditioner.CURRENT_LIMIT,
|
airconditioner.CURRENT_LIMIT,
|
||||||
|
airconditioner.CURRENT_TEMPERATURE,
|
||||||
|
airconditioner.CURRENT_TEMPERATURE_VS,
|
||||||
|
airconditioner.HUMIDITY,
|
||||||
*airconditioner.COVERAGE,
|
*airconditioner.COVERAGE,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -25,5 +25,6 @@ REGISTRY = DeviceRegistry(
|
|||||||
range_caps.COOKTOP_STATUS,
|
range_caps.COOKTOP_STATUS,
|
||||||
range_caps.COOKTOP_SPEC,
|
range_caps.COOKTOP_SPEC,
|
||||||
range_caps.COOKTOP_SAFETY,
|
range_caps.COOKTOP_SAFETY,
|
||||||
|
range_caps.COOKTOP_MONITORING,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -33,7 +33,7 @@ REGISTRY = DeviceRegistry(
|
|||||||
]),
|
]),
|
||||||
pattern_capabilities=[
|
pattern_capabilities=[
|
||||||
fridge.TEMP_CURRENT_GENERIC,
|
fridge.TEMP_CURRENT_GENERIC,
|
||||||
fridge.TEMP_SETPOINT_GENERIC,
|
fridge.TEMP_SETPOINT,
|
||||||
fridge.ICEMAKER_GENERIC,
|
fridge.ICEMAKER_GENERIC,
|
||||||
fridge.DOOR_GENERIC,
|
fridge.DOOR_GENERIC,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ by_type registry.
|
|||||||
"""
|
"""
|
||||||
from ..capability import Capability
|
from ..capability import Capability
|
||||||
from ..entities import BinarySensorDesc, ClimateDesc, SensorDesc, SwitchDesc
|
from ..entities import BinarySensorDesc, ClimateDesc, SensorDesc, SwitchDesc
|
||||||
|
from .common import normalize_temp_unit
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Canonical AC resource hrefs. The climate entity (climate.py) binds the
|
# Canonical AC resource hrefs. The climate entity (climate.py) binds the
|
||||||
@@ -50,6 +51,25 @@ def _num(v):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
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 _filter_usage_percent(rep):
|
def _filter_usage_percent(rep):
|
||||||
"""Filter usage as a percentage of rated capacity. The device reports
|
"""Filter usage as a percentage of rated capacity. The device reports
|
||||||
`filterUsage` as a raw count in `filterCapacityUnit` (Hours here, e.g.
|
`filterUsage` as a raw count in `filterCapacityUnit` (Hours here, e.g.
|
||||||
@@ -199,6 +219,48 @@ CURRENT_LIMIT = Capability(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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
|
# AC-scoped coverage: the CLIMATE_CONSUMED_HREFS above (read by the climate
|
||||||
# entity) plus vendor duplicates / all-zero-ambiguous / plumbing resources.
|
# entity) plus vendor duplicates / all-zero-ambiguous / plumbing resources.
|
||||||
@@ -221,8 +283,10 @@ _AC_IGNORED = [
|
|||||||
# All-zero and ambiguously encoded on this model (2-value arrays); the
|
# All-zero and ambiguously encoded on this model (2-value arrays); the
|
||||||
# 'don't guess' rule -- leave unmodeled rather than invent entities.
|
# 'don't guess' rule -- leave unmodeled rather than invent entities.
|
||||||
'/sensors/vs/0',
|
'/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/0',
|
||||||
'/humidity/vs/0',
|
|
||||||
# Presence-personalization plumbing (empty item list here).
|
# Presence-personalization plumbing (empty item list here).
|
||||||
'/personality/presence/vs/0',
|
'/personality/presence/vs/0',
|
||||||
# --- TP1X/TP2X-class housekeeping / opaque blobs. These carry no
|
# --- TP1X/TP2X-class housekeeping / opaque blobs. These carry no
|
||||||
@@ -251,8 +315,11 @@ _AC_IGNORED = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Built as bare no-entity caps; folded into the AC registry (not global).
|
# 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 = [
|
COVERAGE = [
|
||||||
Capability(href=h, poll_tier='warm') for h in CLIMATE_CONSUMED_HREFS
|
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
|
Capability(href=h) for h in _AC_IGNORED
|
||||||
]
|
]
|
||||||
@@ -59,9 +59,21 @@ def _ml_to_l(v):
|
|||||||
|
|
||||||
|
|
||||||
def _active_alarm_codes(items):
|
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):
|
if not items or not isinstance(items, list):
|
||||||
return 'none'
|
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'
|
return ', '.join(codes) if codes else 'none'
|
||||||
|
|
||||||
|
|
||||||
@@ -92,6 +104,52 @@ def merge_options_field(cached, new_tokens):
|
|||||||
return merged
|
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):
|
def sensor_item_value(items, sensor_type, index=0):
|
||||||
"""Pull one reading out of a `/sensors/vs/0`-style items[] list -- each
|
"""Pull one reading out of a `/sensors/vs/0`-style items[] list -- each
|
||||||
item is `{type, value: [...]}`; `index` picks which slot of a possibly
|
item is `{type, value: [...]}`; `index` picks which slot of a possibly
|
||||||
@@ -128,10 +186,17 @@ def sensor_item_value(items, sensor_type, index=0):
|
|||||||
POWER_GENERIC = Capability(
|
POWER_GENERIC = Capability(
|
||||||
href='/power/0',
|
href='/power/0',
|
||||||
entities=(
|
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',
|
SwitchDesc(key='power_switch', field='value',
|
||||||
value_fn=lambda v: bool(v),
|
value_fn=lambda v: bool(v),
|
||||||
|
exists_fn=_power_switch_exists,
|
||||||
write_fn=lambda p, rep, href=None: (
|
write_fn=lambda p, rep, href=None: (
|
||||||
['power', '0'], {'value': p == 'On'})),
|
['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 +206,14 @@ POWER_VS_FALLBACK = Capability(
|
|||||||
entities=(
|
entities=(
|
||||||
SwitchDesc(key='power_switch', field='x.com.samsung.da.power',
|
SwitchDesc(key='power_switch', field='x.com.samsung.da.power',
|
||||||
value_fn=lambda v: v == 'On',
|
value_fn=lambda v: v == 'On',
|
||||||
|
exists_fn=_power_switch_exists,
|
||||||
write_fn=lambda p, rep, href=None: (
|
write_fn=lambda p, rep, href=None: (
|
||||||
['power', 'vs', '0'],
|
['power', 'vs', '0'],
|
||||||
{'x.com.samsung.da.power': 'On' if p == 'On' else 'Off'})),
|
{'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 +262,20 @@ def remote_control_enabled(resources: dict) -> bool:
|
|||||||
return True
|
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(
|
REMOTE_CONTROL_GENERIC = Capability(
|
||||||
href='/remotectrl/0',
|
href='/remotectrl/0',
|
||||||
poll_tier='warm',
|
poll_tier='warm',
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ DRYER_SETTINGS = Capability(
|
|||||||
# under entity.select.dryer_cycle (Table_03, DV5000-class, captured
|
# under entity.select.dryer_cycle (Table_03, DV5000-class, captured
|
||||||
# 2026-05-29). Codes 0x21 and 0x4C appear in the issue #14 DV90BB5245AES1
|
# 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
|
# 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
|
# course and is ignored (ignored.py) -- the mirror of how /st/washercourse/vs/0
|
||||||
# is ignored for washers.
|
# is ignored for washers.
|
||||||
DRYER_COURSE = Capability(
|
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=None,
|
||||||
href_prefix='/temperature/desired/',
|
href_prefix='/temperature/desired/',
|
||||||
strip_prefix_in_key=True,
|
strip_prefix_in_key=True,
|
||||||
@@ -73,10 +104,7 @@ TEMP_SETPOINT_GENERIC = Capability(
|
|||||||
use_instance_name=True, device_class='temperature', unit_fn=_temp_unit,
|
use_instance_name=True, device_class='temperature', unit_fn=_temp_unit,
|
||||||
native_min=-20.0, native_max=50.0,
|
native_min=-20.0, native_max=50.0,
|
||||||
range_field='range', entity_category='config',
|
range_field='range', entity_category='config',
|
||||||
write_fn=lambda p, rep, href=None: (
|
write_fn=_temp_setpoint_write),
|
||||||
[s for s in href.strip('/').split('/') if s],
|
|
||||||
{'temperature': int(round(float(p)))}
|
|
||||||
) if href else None),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -133,3 +133,22 @@ COOKTOP_SAFETY = Capability(
|
|||||||
value_fn=lambda v: (v or {}).get('state') == 'on'),
|
value_fn=lambda v: (v or {}).get('state') == 'on'),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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'),
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -45,6 +45,14 @@ from .laundry import (
|
|||||||
# (issue #22). Combo units carry their own course set, so these codes
|
# (issue #22). Combo units carry their own course set, so these codes
|
||||||
# don't imply anything about '1F' on a plain washer.
|
# 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
|
# 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
|
# 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
|
# course, '65', isn't even in the list above; models with 'AI Wash'/'Mixed
|
||||||
|
|||||||
@@ -61,6 +61,9 @@
|
|||||||
"power_state": {
|
"power_state": {
|
||||||
"name": "Power state"
|
"name": "Power state"
|
||||||
},
|
},
|
||||||
|
"power_switch": {
|
||||||
|
"name": "Power"
|
||||||
|
},
|
||||||
"rapid_freezing": {
|
"rapid_freezing": {
|
||||||
"name": "Rapid freezing"
|
"name": "Rapid freezing"
|
||||||
},
|
},
|
||||||
@@ -101,6 +104,7 @@
|
|||||||
},
|
},
|
||||||
"preset_mode": {
|
"preset_mode": {
|
||||||
"state": {
|
"state": {
|
||||||
|
"ai_comfort": "AI Comfort",
|
||||||
"quiet": "Quiet",
|
"quiet": "Quiet",
|
||||||
"smart": "Smart",
|
"smart": "Smart",
|
||||||
"speed": "Speed"
|
"speed": "Speed"
|
||||||
@@ -213,6 +217,8 @@
|
|||||||
"dryer_cycle_table_03": {
|
"dryer_cycle_table_03": {
|
||||||
"name": "Cycle",
|
"name": "Cycle",
|
||||||
"state": {
|
"state": {
|
||||||
|
"01": "Normal",
|
||||||
|
"06": "Time dry",
|
||||||
"16": "Cotton",
|
"16": "Cotton",
|
||||||
"18": "Synthetics",
|
"18": "Synthetics",
|
||||||
"19": "Delicates",
|
"19": "Delicates",
|
||||||
@@ -397,30 +403,37 @@
|
|||||||
"27": "Rinse+Spin",
|
"27": "Rinse+Spin",
|
||||||
"28": "Drain/Spin",
|
"28": "Drain/Spin",
|
||||||
"29": "Drum Clean+",
|
"29": "Drum Clean+",
|
||||||
|
"2a": "Jeans",
|
||||||
|
"2b": "AI Wash",
|
||||||
"2d": "Silent Wash",
|
"2d": "Silent Wash",
|
||||||
"2e": "Baby Care",
|
"2e": "Baby Care",
|
||||||
"2f": "Activewear",
|
"2f": "Activewear",
|
||||||
"30": "Cloudy Day",
|
"30": "Cloudy Day",
|
||||||
"32": "Shirts",
|
"32": "Shirts",
|
||||||
"33": "Bedding",
|
"33": "Bedding",
|
||||||
|
"34": "Mixed",
|
||||||
"36": "Wash+Dry",
|
"36": "Wash+Dry",
|
||||||
"37": "Air Wash",
|
"37": "Air Wash",
|
||||||
"38": "Cotton Dry",
|
"38": "Cotton Dry",
|
||||||
"39": "Synthetics Dry",
|
"39": "Synthetics Dry",
|
||||||
|
"3a": "Drum Clean",
|
||||||
|
"52": "Eco Cold",
|
||||||
"53": "Heavy Duty",
|
"53": "Heavy Duty",
|
||||||
|
"54": "Towels",
|
||||||
"55": "Activewear",
|
"55": "Activewear",
|
||||||
"57": "Delicate",
|
"57": "Delicate",
|
||||||
"5e": "Rinse+Spin",
|
"5e": "Rinse+Spin",
|
||||||
|
"60": "Self Clean+",
|
||||||
"65": "Colors",
|
"65": "Colors",
|
||||||
"66": "Denim",
|
"66": "Denim",
|
||||||
"7c": "Whites",
|
"7c": "Whites",
|
||||||
"7d": "Bedding/Waterproof",
|
"7d": "Bedding/Waterproof",
|
||||||
"7e": "Self-Clean",
|
"7e": "Self-Clean",
|
||||||
|
"7f": "Wool/Delicate",
|
||||||
"86": "Deep Wash",
|
"86": "Deep Wash",
|
||||||
|
"87": "Download",
|
||||||
"8f": "Intense Cold",
|
"8f": "Intense Cold",
|
||||||
"96": "Less Microfiber",
|
"96": "Less Microfiber"
|
||||||
"2b": "AI Wash",
|
|
||||||
"2a": "Jeans"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"washer_dry_level": {
|
"washer_dry_level": {
|
||||||
@@ -466,6 +479,9 @@
|
|||||||
"poll": "Polling"
|
"poll": "Polling"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cooktop_running_state": {
|
||||||
|
"name": "Cooktop running state"
|
||||||
|
},
|
||||||
"cooktop_state": {
|
"cooktop_state": {
|
||||||
"name": "Cooktop state"
|
"name": "Cooktop state"
|
||||||
},
|
},
|
||||||
@@ -475,6 +491,9 @@
|
|||||||
"current_temp_c": {
|
"current_temp_c": {
|
||||||
"name": "Temperature"
|
"name": "Temperature"
|
||||||
},
|
},
|
||||||
|
"current_temperature_c": {
|
||||||
|
"name": "Temperature"
|
||||||
|
},
|
||||||
"diagnosis": {
|
"diagnosis": {
|
||||||
"name": "Diagnosis"
|
"name": "Diagnosis"
|
||||||
},
|
},
|
||||||
@@ -547,6 +566,9 @@
|
|||||||
"hood_filter_usage": {
|
"hood_filter_usage": {
|
||||||
"name": "Filter usage"
|
"name": "Filter usage"
|
||||||
},
|
},
|
||||||
|
"humidity": {
|
||||||
|
"name": "Humidity"
|
||||||
|
},
|
||||||
"instance_temperature": {
|
"instance_temperature": {
|
||||||
"name": "{instance_name} temperature"
|
"name": "{instance_name} temperature"
|
||||||
},
|
},
|
||||||
@@ -625,6 +647,9 @@
|
|||||||
"super_fine_dust": {
|
"super_fine_dust": {
|
||||||
"name": "Super fine dust"
|
"name": "Super fine dust"
|
||||||
},
|
},
|
||||||
|
"warming_center_state": {
|
||||||
|
"name": "Warming center state"
|
||||||
|
},
|
||||||
"water_liters": {
|
"water_liters": {
|
||||||
"name": "Water consumption"
|
"name": "Water consumption"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,6 +61,9 @@
|
|||||||
"power_state": {
|
"power_state": {
|
||||||
"name": "Voedingsstatus"
|
"name": "Voedingsstatus"
|
||||||
},
|
},
|
||||||
|
"power_switch": {
|
||||||
|
"name": "Voeding"
|
||||||
|
},
|
||||||
"rapid_freezing": {
|
"rapid_freezing": {
|
||||||
"name": "Snelvriezen"
|
"name": "Snelvriezen"
|
||||||
},
|
},
|
||||||
@@ -101,6 +104,7 @@
|
|||||||
},
|
},
|
||||||
"preset_mode": {
|
"preset_mode": {
|
||||||
"state": {
|
"state": {
|
||||||
|
"ai_comfort": "AI-comfort",
|
||||||
"quiet": "Stil",
|
"quiet": "Stil",
|
||||||
"smart": "Slim",
|
"smart": "Slim",
|
||||||
"speed": "Snel"
|
"speed": "Snel"
|
||||||
@@ -213,6 +217,8 @@
|
|||||||
"dryer_cycle_table_03": {
|
"dryer_cycle_table_03": {
|
||||||
"name": "Programma",
|
"name": "Programma",
|
||||||
"state": {
|
"state": {
|
||||||
|
"01": "Normaal",
|
||||||
|
"06": "Tijdprogramma",
|
||||||
"16": "Katoen",
|
"16": "Katoen",
|
||||||
"18": "Synthetisch",
|
"18": "Synthetisch",
|
||||||
"19": "Fijne was",
|
"19": "Fijne was",
|
||||||
@@ -397,30 +403,37 @@
|
|||||||
"27": "Spoelen+centrifugeren",
|
"27": "Spoelen+centrifugeren",
|
||||||
"28": "Afpompen/centrifugeren",
|
"28": "Afpompen/centrifugeren",
|
||||||
"29": "Drum Clean+",
|
"29": "Drum Clean+",
|
||||||
|
"2a": "Spijkerbroek",
|
||||||
|
"2b": "AI Wash",
|
||||||
"2d": "Stille was",
|
"2d": "Stille was",
|
||||||
"2e": "Babyverzorging",
|
"2e": "Babyverzorging",
|
||||||
"2f": "Sportkleding",
|
"2f": "Sportkleding",
|
||||||
"30": "Bewolkte dag",
|
"30": "Bewolkte dag",
|
||||||
"32": "Overhemden",
|
"32": "Overhemden",
|
||||||
"33": "Beddengoed",
|
"33": "Beddengoed",
|
||||||
|
"34": "Gemengd",
|
||||||
"36": "Wassen+drogen",
|
"36": "Wassen+drogen",
|
||||||
"37": "Air Wash",
|
"37": "Air Wash",
|
||||||
"38": "Katoen drogen",
|
"38": "Katoen drogen",
|
||||||
"39": "Synthetisch drogen",
|
"39": "Synthetisch drogen",
|
||||||
|
"3a": "Trommel reinigen",
|
||||||
|
"52": "Eco koud",
|
||||||
"53": "Intensief",
|
"53": "Intensief",
|
||||||
|
"54": "Handdoeken",
|
||||||
"55": "Sportkleding",
|
"55": "Sportkleding",
|
||||||
"57": "Fijn",
|
"57": "Fijn",
|
||||||
"5e": "Spoelen+centrifugeren",
|
"5e": "Spoelen+centrifugeren",
|
||||||
|
"60": "Zelfreinigend+",
|
||||||
"65": "Bonte was",
|
"65": "Bonte was",
|
||||||
"66": "Spijkergoed",
|
"66": "Spijkergoed",
|
||||||
"7c": "Witte was",
|
"7c": "Witte was",
|
||||||
"7d": "Beddengoed/Waterdicht",
|
"7d": "Beddengoed/Waterdicht",
|
||||||
"7e": "Zelfreinigend",
|
"7e": "Zelfreinigend",
|
||||||
|
"7f": "Wol/Fijn",
|
||||||
"86": "Diep wassen",
|
"86": "Diep wassen",
|
||||||
|
"87": "Downloaden",
|
||||||
"8f": "Intensief koud",
|
"8f": "Intensief koud",
|
||||||
"96": "Minder microvezels",
|
"96": "Minder microvezels"
|
||||||
"2b": "AI Wash",
|
|
||||||
"2a": "Spijkerbroek"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"washer_dry_level": {
|
"washer_dry_level": {
|
||||||
@@ -466,6 +479,9 @@
|
|||||||
"poll": "Polling"
|
"poll": "Polling"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cooktop_running_state": {
|
||||||
|
"name": "Loopstatus kookplaat"
|
||||||
|
},
|
||||||
"cooktop_state": {
|
"cooktop_state": {
|
||||||
"name": "Status kookplaat"
|
"name": "Status kookplaat"
|
||||||
},
|
},
|
||||||
@@ -475,6 +491,9 @@
|
|||||||
"current_temp_c": {
|
"current_temp_c": {
|
||||||
"name": "Temperatuur"
|
"name": "Temperatuur"
|
||||||
},
|
},
|
||||||
|
"current_temperature_c": {
|
||||||
|
"name": "Temperatuur"
|
||||||
|
},
|
||||||
"diagnosis": {
|
"diagnosis": {
|
||||||
"name": "Diagnose"
|
"name": "Diagnose"
|
||||||
},
|
},
|
||||||
@@ -547,6 +566,9 @@
|
|||||||
"hood_filter_usage": {
|
"hood_filter_usage": {
|
||||||
"name": "Filterverbruik"
|
"name": "Filterverbruik"
|
||||||
},
|
},
|
||||||
|
"humidity": {
|
||||||
|
"name": "Luchtvochtigheid"
|
||||||
|
},
|
||||||
"instance_temperature": {
|
"instance_temperature": {
|
||||||
"name": "Temperatuur {instance_name}"
|
"name": "Temperatuur {instance_name}"
|
||||||
},
|
},
|
||||||
@@ -625,6 +647,9 @@
|
|||||||
"super_fine_dust": {
|
"super_fine_dust": {
|
||||||
"name": "Ultrafijnstof"
|
"name": "Ultrafijnstof"
|
||||||
},
|
},
|
||||||
|
"warming_center_state": {
|
||||||
|
"name": "Status warmhoudzone"
|
||||||
|
},
|
||||||
"water_liters": {
|
"water_liters": {
|
||||||
"name": "Waterverbruik"
|
"name": "Waterverbruik"
|
||||||
},
|
},
|
||||||
|
|||||||
+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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+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**"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+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": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+2
@@ -6,9 +6,11 @@
|
|||||||
"alarm_code",
|
"alarm_code",
|
||||||
"auto_clean",
|
"auto_clean",
|
||||||
"climate",
|
"climate",
|
||||||
|
"current_temperature_c",
|
||||||
"diagnosis_status",
|
"diagnosis_status",
|
||||||
"energy_kwh",
|
"energy_kwh",
|
||||||
"energy_saved_kwh",
|
"energy_saved_kwh",
|
||||||
|
"humidity",
|
||||||
"power_watts"
|
"power_watts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -5,10 +5,12 @@
|
|||||||
"alarm_code",
|
"alarm_code",
|
||||||
"auto_clean",
|
"auto_clean",
|
||||||
"climate",
|
"climate",
|
||||||
|
"current_temperature_c",
|
||||||
"diagnosis_status",
|
"diagnosis_status",
|
||||||
"energy_kwh",
|
"energy_kwh",
|
||||||
"energy_saved_kwh",
|
"energy_saved_kwh",
|
||||||
"firmware_update",
|
"firmware_update",
|
||||||
|
"humidity",
|
||||||
"mute_once",
|
"mute_once",
|
||||||
"power_watts"
|
"power_watts"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
"alarm_code",
|
"alarm_code",
|
||||||
"auto_clean",
|
"auto_clean",
|
||||||
"climate",
|
"climate",
|
||||||
|
"current_temperature_c",
|
||||||
"display_light",
|
"display_light",
|
||||||
"energy_kwh",
|
"energy_kwh",
|
||||||
"energy_saved_kwh",
|
"energy_saved_kwh",
|
||||||
"firmware_update",
|
"firmware_update",
|
||||||
|
"humidity",
|
||||||
"mute_once",
|
"mute_once",
|
||||||
"selfcheck_error",
|
"selfcheck_error",
|
||||||
"selfcheck_result",
|
"selfcheck_result",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"climate",
|
"climate",
|
||||||
"current_limit_enabled",
|
"current_limit_enabled",
|
||||||
"current_limit_level",
|
"current_limit_level",
|
||||||
|
"current_temperature_c",
|
||||||
"display_light",
|
"display_light",
|
||||||
"energy_kwh",
|
"energy_kwh",
|
||||||
"energy_saved_kwh",
|
"energy_saved_kwh",
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
"alarm_code",
|
"alarm_code",
|
||||||
"auto_clean",
|
"auto_clean",
|
||||||
"climate",
|
"climate",
|
||||||
|
"current_temperature_c",
|
||||||
"energy_kwh",
|
"energy_kwh",
|
||||||
"firmware_update",
|
"firmware_update",
|
||||||
|
"humidity",
|
||||||
"mute_once",
|
"mute_once",
|
||||||
"power_watts"
|
"power_watts"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"state_keys": [
|
||||||
|
"air_filter_status",
|
||||||
|
"air_filter_usage",
|
||||||
|
"air_purify",
|
||||||
|
"alarm_code",
|
||||||
|
"auto_clean",
|
||||||
|
"climate",
|
||||||
|
"current_temperature_c",
|
||||||
|
"diagnosis_status",
|
||||||
|
"energy_kwh",
|
||||||
|
"humidity",
|
||||||
|
"power_watts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"state_keys": [
|
||||||
|
"air_filter_status",
|
||||||
|
"air_filter_usage",
|
||||||
|
"alarm_code",
|
||||||
|
"auto_clean",
|
||||||
|
"climate",
|
||||||
|
"display_light",
|
||||||
|
"energy_kwh",
|
||||||
|
"energy_saved_kwh",
|
||||||
|
"firmware_update",
|
||||||
|
"mute_once",
|
||||||
|
"power_watts",
|
||||||
|
"selfcheck_error",
|
||||||
|
"selfcheck_result",
|
||||||
|
"selfcheck_status"
|
||||||
|
]
|
||||||
|
}
|
||||||
+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"
|
||||||
|
]
|
||||||
|
}
|
||||||
+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"
|
||||||
|
]
|
||||||
|
}
|
||||||
+201
@@ -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": {
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.alarms"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.s"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Alarm",
|
||||||
|
"x.com.samsung.da.alarmType": "Device",
|
||||||
|
"x.com.samsung.da.code": "OV_E_OFF",
|
||||||
|
"x.com.samsung.da.triggeredTime": "2024-06-03T04:35:25"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/configuration/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/connected/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.connected": "On"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/cooktopmonitoring/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.warmingCenterState": "Off",
|
||||||
|
"x.com.samsung.da.cooktopMonitoring": "0",
|
||||||
|
"x.com.samsung.da.cooktopRunningState": "Ready",
|
||||||
|
"supportedHoodLampStateList": [
|
||||||
|
"Run",
|
||||||
|
"Ready"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/doors/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Door",
|
||||||
|
"x.com.samsung.da.openState": "Close",
|
||||||
|
"x.com.samsung.da.lock": "Unlock"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.doors"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.s"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/kidslock/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.kidsLock": "Ready"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"Bake",
|
||||||
|
"Broil",
|
||||||
|
"ConvectionBake",
|
||||||
|
"ConvectionRoast",
|
||||||
|
"KeepWarm",
|
||||||
|
"BreadProof",
|
||||||
|
"AirFryer",
|
||||||
|
"Dehydrate",
|
||||||
|
"SelfClean",
|
||||||
|
"SteamClean"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modes": [
|
||||||
|
"NoOperation"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.options": [
|
||||||
|
"DeviceType_NE8411B-/AC0",
|
||||||
|
"SettingPossible_0",
|
||||||
|
"meatprobe_disconnected",
|
||||||
|
"UpperLamp_Off",
|
||||||
|
"Sound_On",
|
||||||
|
"AdjustingTemp_11",
|
||||||
|
"Sabbath_Off",
|
||||||
|
"EnergySaving_On",
|
||||||
|
"BurnerOnAlert_On"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.defaultMode": "ConvectionBake",
|
||||||
|
"x.com.samsung.da.modeSpec": "[{\"mode\":\"Bake\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"175\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"350\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"Broil\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"61441\",\"tempMaxC\":\"61442\",\"tempDefaultC\":\"61441\",\"tempListLengthC\":\"2\",\"tempListDataC\":[\"61441\",\"61442\"],\"tempMinF\":\"61441\",\"tempMaxF\":\"61442\",\"tempDefaultF\":\"61441\",\"tempListLengthF\":\"2\",\"tempListDataF\":[\"61441\",\"61442\"],\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"ConvectionBake\",\"version\":\"0100\",\"default\":\"Default\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"160\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"325\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"ConvectionRoast\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"160\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"325\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"KeepWarm\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"80\",\"tempDefaultC\":\"80\",\"tempListLengthC\":\"1\",\"tempListDataC\":[\"80\"],\"tempMinF\":\"175\",\"tempMaxF\":\"175\",\"tempDefaultF\":\"175\",\"tempListLengthF\":\"1\",\"tempListDataF\":[\"175\"],\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"BreadProof\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"35\",\"tempMaxC\":\"35\",\"tempDefaultC\":\"35\",\"tempListLengthC\":\"1\",\"tempListDataC\":[\"35\"],\"tempMinF\":\"95\",\"tempMaxF\":\"95\",\"tempDefaultF\":\"95\",\"tempListLengthF\":\"1\",\"tempListDataF\":[\"95\"],\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"AirFryer\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"175\",\"tempMaxC\":\"260\",\"tempDefaultC\":\"220\",\"tempListLengthC\":\"0\",\"tempMinF\":\"350\",\"tempMaxF\":\"500\",\"tempDefaultF\":\"425\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:Line truncated
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.mode"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.a"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/operational/state/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.state": "Ready",
|
||||||
|
"x.com.samsung.da.operationTime": "00:00:00",
|
||||||
|
"x.com.samsung.da.remainingTime": "00:00:00",
|
||||||
|
"x.com.samsung.da.progressPercentage": "1",
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.operation"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.a"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/otninformation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.target": "",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/oven/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.state": "Ready",
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.oven"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.s"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/power/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.power": "On"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/remotectrl/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.remoteControlEnabled": "false",
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.configuration"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.s"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperatures/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Temperature",
|
||||||
|
"x.com.samsung.da.desired": "0",
|
||||||
|
"x.com.samsung.da.current": "80",
|
||||||
|
"x.com.samsung.da.increment": "1",
|
||||||
|
"x.com.samsung.da.unit": "Celsius"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.temperatures"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.a"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -508,3 +508,19 @@ async def test_options_flow_finish_preserves_existing_options(
|
|||||||
|
|
||||||
assert result['type'] == FlowResultType.CREATE_ENTRY
|
assert result['type'] == FlowResultType.CREATE_ENTRY
|
||||||
assert entry.options[CONF_BYPASS_REMOTE_CONTROL] is True
|
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.coordinator import LocalThingsCoordinator
|
||||||
from custom_components.localthings.registry.capabilities.common import (
|
from custom_components.localthings.registry.capabilities.common import (
|
||||||
remote_control_enabled,
|
remote_control_enabled,
|
||||||
|
remote_control_required_for_write,
|
||||||
)
|
)
|
||||||
from custom_components.localthings.observe import MODE_OBSERVE, MODE_POLL, PUSH_HEALTH_WINDOW_S
|
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'
|
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(
|
def test_run_discovery_detects_cooktop_via_resource_signature(
|
||||||
hass: HomeAssistant, mock_entry
|
hass: HomeAssistant, mock_entry
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -880,6 +904,38 @@ class TestRemoteControlEnabled:
|
|||||||
assert remote_control_enabled({}) is True
|
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(
|
async def test_send_command_blocked_when_remote_control_disabled(
|
||||||
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session
|
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1024,3 +1080,95 @@ async def test_send_command_bypasses_remote_control_when_option_enabled(
|
|||||||
await coordinator.async_send_command(bound, 5)
|
await coordinator.async_send_command(bound, 5)
|
||||||
|
|
||||||
assert coordinator._cache.get('/some/path') == {'value': 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
|
||||||
@@ -114,11 +114,13 @@ def test_climate_write_targets():
|
|||||||
def test_climate_consumed_hrefs_declared_as_coverage():
|
def test_climate_consumed_hrefs_declared_as_coverage():
|
||||||
"""The climate-consumed and ambiguous hrefs are declared in the AC registry
|
"""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
|
(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()
|
reg, _ = _ac()
|
||||||
for href in ('/power/0', '/power/vs/0', '/temperature/desired/0',
|
for href in ('/power/0', '/power/vs/0', '/temperature/desired/0',
|
||||||
'/wind/strength/vs/0', '/mode/convenient/vs/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)
|
caps = reg.capabilities.get(href)
|
||||||
assert caps, href
|
assert caps, href
|
||||||
assert all(c.entities == () for c in caps), href
|
assert all(c.entities == () for c in caps), href
|
||||||
@@ -257,3 +259,50 @@ def test_current_limit_is_read_only():
|
|||||||
than a guessed writable control."""
|
than a guessed writable control."""
|
||||||
for desc in airconditioner.CURRENT_LIMIT.entities:
|
for desc in airconditioner.CURRENT_LIMIT.entities:
|
||||||
assert getattr(desc, 'write_fn', None) is None
|
assert getattr(desc, 'write_fn', None) is None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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
|
||||||
@@ -111,6 +111,27 @@ class TestWasherRegistry:
|
|||||||
assert href in registry.capabilities, f"{href} missing from washer registry"
|
assert href in registry.capabilities, f"{href} missing from washer registry"
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
class TestForDeviceByModel:
|
||||||
"""Fallback device-type detection for hardware without oneUiVersion."""
|
"""Fallback device-type detection for hardware without oneUiVersion."""
|
||||||
|
|
||||||
@@ -142,6 +163,20 @@ class TestForDeviceByModel:
|
|||||||
assert reg is not None
|
assert reg is not None
|
||||||
assert reg.name == 'dryer'
|
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):
|
def test_dishwasher_not_misdetected_as_washer(self):
|
||||||
"""Dishwasher's modelNum contains the substring 'WW' -- must not
|
"""Dishwasher's modelNum contains the substring 'WW' -- must not
|
||||||
be misrouted by a naive substring match."""
|
be misrouted by a naive substring match."""
|
||||||
@@ -202,6 +237,19 @@ class TestForDeviceByModel:
|
|||||||
assert reg is not None
|
assert reg is not None
|
||||||
assert reg.name == 'water_purifier'
|
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_cooktop_via_legacy_model_description(self):
|
def test_cooktop_via_legacy_model_description(self):
|
||||||
"""Older cooktops identify themselves as ARTIK051_GLOBAL_COOKTOP."""
|
"""Older cooktops identify themselves as ARTIK051_GLOBAL_COOKTOP."""
|
||||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||||
@@ -296,3 +344,46 @@ class TestForDeviceByResources:
|
|||||||
reg = for_device_by_resources(resources)
|
reg = for_device_by_resources(resources)
|
||||||
assert reg is not None
|
assert reg is not None
|
||||||
assert reg.name == 'range_hood'
|
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,40 @@
|
|||||||
|
"""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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_still_maps_to_heat_cool():
|
||||||
|
"""'Auto' is unchanged -- AIComfort is handled separately, not folded
|
||||||
|
into this map."""
|
||||||
|
assert _DEVICE_TO_HVAC['Auto'] == HVACMode.HEAT_COOL
|
||||||
|
|
||||||
|
|
||||||
|
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_not_writable_via_hvac_mode():
|
||||||
|
"""HVACMode.AUTO has no _DEVICE_TO_HVAC entry, so it's unreachable via
|
||||||
|
async_set_hvac_mode -- entered/left only through the ai_comfort preset."""
|
||||||
|
assert HVACMode.AUTO not in _HVAC_TO_DEVICE
|
||||||
|
|
||||||
|
|
||||||
|
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_preset_ai_comfort_constant():
|
||||||
|
assert PRESET_AI_COMFORT == 'ai_comfort'
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""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: PR #91 replaces
|
||||||
|
climate.py's static _DEVICE_TO_PRESET table with a generic resolver that
|
||||||
|
reads any device preset code straight off the unit's own supportedModes,
|
||||||
|
which 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.capabilities import common
|
||||||
from custom_components.localthings.registry.discovery import discover
|
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():
|
def _reg():
|
||||||
@@ -65,7 +67,7 @@ class TestMergeOptionsField:
|
|||||||
class TestPowerFallback:
|
class TestPowerFallback:
|
||||||
def test_generic_href_read_write(self):
|
def test_generic_href_read_write(self):
|
||||||
assert common.POWER_GENERIC.href == '/power/0'
|
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(True) is True
|
||||||
assert desc.value_fn(False) is False
|
assert desc.value_fn(False) is False
|
||||||
path, body = desc.write_fn('On', {})
|
path, body = desc.write_fn('On', {})
|
||||||
@@ -80,13 +82,72 @@ class TestPowerFallback:
|
|||||||
{}, {'/power/0': {}, '/power/vs/0': {}}) is False
|
{}, {'/power/0': {}, '/power/vs/0': {}}) is False
|
||||||
|
|
||||||
def test_vs_fallback_read_write(self):
|
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('On') is True
|
||||||
assert desc.value_fn('Off') is False
|
assert desc.value_fn('Off') is False
|
||||||
path, body = desc.write_fn('On', {})
|
path, body = desc.write_fn('On', {})
|
||||||
assert path == ['power', 'vs', '0']
|
assert path == ['power', 'vs', '0']
|
||||||
assert body == {'x.com.samsung.da.power': 'On'}
|
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:
|
class TestKidsLockFallback:
|
||||||
def test_generic_read_write(self):
|
def test_generic_read_write(self):
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ class TestTempCurrentGeneric:
|
|||||||
assert desc.unit_fn({'temperature': 5.0}) == '°F'
|
assert desc.unit_fn({'temperature': 5.0}) == '°F'
|
||||||
|
|
||||||
|
|
||||||
class TestTempSetpointGeneric:
|
class TestTempSetpoint:
|
||||||
def test_unit_reads_celsius(self):
|
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'
|
assert desc.unit_fn({'temperature': -19.0, 'units': 'C'}) == '°C'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,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():
|
def test_registry_reproduces_golden_state_keys_for_airconditioner():
|
||||||
from tests.conftest import _load_device
|
from tests.conftest import _load_device
|
||||||
resources = _load_device('airconditioner')
|
resources = _load_device('airconditioner')
|
||||||
@@ -247,6 +264,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():
|
def test_registry_reproduces_golden_state_keys_for_tp1x_rac():
|
||||||
"""TP1X_DA-AC-RAC-01001_0000 (issue #38) -- fuller RAC board with display
|
"""TP1X_DA-AC-RAC-01001_0000 (issue #38) -- fuller RAC board with display
|
||||||
light, self-check, mute-once, and a current-limit setting."""
|
light, self-check, mute-once, and a current-limit setting."""
|
||||||
@@ -261,6 +295,23 @@ def test_registry_reproduces_golden_state_keys_for_tp1x_rac():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
def test_registry_reproduces_golden_state_keys_for_range():
|
||||||
"""Range/cooktop-oven combo (model TP1X_DA-KS-RANGE-0102X, issue #44) --
|
"""Range/cooktop-oven combo (model TP1X_DA-KS-RANGE-0102X, issue #44) --
|
||||||
reports no oneUiVersion; resolved via the '-RANGE-' modelNum token
|
reports no oneUiVersion; resolved via the '-RANGE-' modelNum token
|
||||||
@@ -278,6 +329,26 @@ def test_registry_reproduces_golden_state_keys_for_range():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
def test_registry_reproduces_golden_state_keys_for_air_purifier():
|
||||||
"""ARTIK051_TVTL_18K (issue #56) -- reports no oneUiVersion; resolved via
|
"""ARTIK051_TVTL_18K (issue #56) -- reports no oneUiVersion; resolved via
|
||||||
the '_TVTL_' modelNum token fallback in for_device_by_model."""
|
the '_TVTL_' modelNum token fallback in for_device_by_model."""
|
||||||
|
|||||||
@@ -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,4 @@ def test_all_entity_state_translation_keys_are_lowercase():
|
|||||||
for translation in platform.values():
|
for translation in platform.values():
|
||||||
for state_key in translation.get("state", {}):
|
for state_key in translation.get("state", {}):
|
||||||
assert state_key == state_key.lower()
|
assert state_key == state_key.lower()
|
||||||
|
|
||||||
Reference in new issue
Block a user