Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
760d797c12 | ||
|
|
b944549f55 | ||
|
|
94551da4a0 | ||
|
|
f783aa72b9 | ||
|
|
ed461d82cd | ||
|
|
62d4ed673b | ||
|
|
e74089a545 | ||
|
|
b2f3e501e5 | ||
|
|
8ca2c3cb16 | ||
|
|
aec3c5e460 | ||
|
|
321f186b0e | ||
|
|
2f6b7d54e3 | ||
|
|
6e5fee32b8 | ||
|
|
1e9437ef3c | ||
|
|
36f7aa0b8c | ||
|
|
10ab51c7f3 | ||
|
|
8bf9002eb7 | ||
|
|
fb0f24a175 | ||
|
|
5944d14bca | ||
|
|
1493467c63 | ||
|
|
61ee2972dc | ||
|
|
3d5263d792 | ||
|
|
d6639c99d4 | ||
|
|
5d9b470d7e | ||
|
|
9800cd0aa8 | ||
|
|
3469f57080 | ||
|
|
f54daf3608 | ||
|
|
e49b61e01e | ||
|
|
dbe70ffc5b | ||
|
|
7d011bfe89 | ||
|
|
9199fb7711 | ||
|
|
6611f85958 | ||
|
|
8470853fb6 | ||
|
|
e942c822a6 | ||
|
|
4a376a98aa | ||
|
|
bc9d4f4cf5 | ||
|
|
10f2aac0bd | ||
|
|
0cc9d8e511 | ||
|
|
b586277031 | ||
|
|
2a0a9f32ee | ||
|
|
8dadea6796 | ||
|
|
2cd93619a4 | ||
|
|
5a9a002a7f | ||
|
|
0715eb0829 | ||
|
|
43542b146f | ||
|
|
9a899080fa | ||
|
|
88e630b54f | ||
|
|
70a7f861c3 | ||
|
|
ff034905a2 | ||
|
|
0503bda0f3 | ||
|
|
6f53b3043b |
No files matched your search
@@ -19,6 +19,7 @@ Your state stays on your LAN: HA talks to the appliance over a direct DTLS sessi
|
|||||||
| Air conditioner | `by_type/airconditioner.py` |
|
| Air conditioner | `by_type/airconditioner.py` |
|
||||||
| Dryer | `by_type/dryer.py` |
|
| Dryer | `by_type/dryer.py` |
|
||||||
| Oven | `by_type/oven.py` |
|
| Oven | `by_type/oven.py` |
|
||||||
|
| Range / cooktop-oven combo | `by_type/range.py` |
|
||||||
| Dishwasher | `by_type/dishwasher.py` |
|
| Dishwasher | `by_type/dishwasher.py` |
|
||||||
| Refrigerator | `by_type/refrigerator.py` |
|
| Refrigerator | `by_type/refrigerator.py` |
|
||||||
| Washer | `by_type/washer.py` |
|
| Washer | `by_type/washer.py` |
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ from .registry.capabilities.airconditioner import (
|
|||||||
HREF_TEMP_CURRENT as TEMP_CURRENT_HREF,
|
HREF_TEMP_CURRENT as TEMP_CURRENT_HREF,
|
||||||
HREF_TEMP_DESIRED as TEMP_DESIRED_HREF,
|
HREF_TEMP_DESIRED as TEMP_DESIRED_HREF,
|
||||||
HREF_TEMP_CONTROL as TEMP_CONTROL_HREF,
|
HREF_TEMP_CONTROL as TEMP_CONTROL_HREF,
|
||||||
|
HREF_TEMPS_VS as TEMPS_VS_HREF,
|
||||||
HREF_WIND_STRENGTH as WIND_STRENGTH_HREF,
|
HREF_WIND_STRENGTH as WIND_STRENGTH_HREF,
|
||||||
HREF_WIND_DIRECTION as WIND_DIRECTION_HREF,
|
HREF_WIND_DIRECTION as WIND_DIRECTION_HREF,
|
||||||
HREF_CONVENIENT as CONVENIENT_HREF,
|
HREF_CONVENIENT as CONVENIENT_HREF,
|
||||||
@@ -125,6 +126,22 @@ def _num(value):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _temps_vs_item(rep: dict) -> dict:
|
||||||
|
"""First item of the vendor `/temperatures/vs/0` items[] array.
|
||||||
|
|
||||||
|
Newer AC firmware (Tizen Lite, oneUiVersion "7.0 Air conditioner", e.g.
|
||||||
|
model TP1X_DA-AC-RAC-01011) does NOT expose the OCF-standard
|
||||||
|
/temperature/current/0 + /temperature/desired/0 pair; it reports current
|
||||||
|
and target under a single `/temperatures/vs/0` resource whose
|
||||||
|
`x.com.samsung.da.items[0]` carries current/desired/minimum/maximum/
|
||||||
|
increment/unit. Returns {} when absent, so callers fall through cleanly.
|
||||||
|
"""
|
||||||
|
items = rep.get('x.com.samsung.da.items')
|
||||||
|
if isinstance(items, (list, tuple)) and items and isinstance(items[0], dict):
|
||||||
|
return items[0]
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||||
"""Composite climate entity for a Samsung air conditioner."""
|
"""Composite climate entity for a Samsung air conditioner."""
|
||||||
|
|
||||||
@@ -172,24 +189,41 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
|||||||
|
|
||||||
# -- temperature --------------------------------------------------------
|
# -- temperature --------------------------------------------------------
|
||||||
|
|
||||||
|
def _temps_vs(self) -> dict:
|
||||||
|
"""Vendor `/temperatures/vs/0` items[0] (empty {} when absent)."""
|
||||||
|
return _temps_vs_item(self._rep(TEMPS_VS_HREF))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temperature_unit(self) -> str:
|
def temperature_unit(self) -> str:
|
||||||
raw = self._rep(TEMP_DESIRED_HREF).get('units')
|
raw = self._rep(TEMP_DESIRED_HREF).get('units')
|
||||||
|
if raw is None:
|
||||||
|
raw = self._temps_vs().get('x.com.samsung.da.unit')
|
||||||
return (UnitOfTemperature.FAHRENHEIT
|
return (UnitOfTemperature.FAHRENHEIT
|
||||||
if normalize_temp_unit(raw, '°C') == '°F'
|
if normalize_temp_unit(raw, '°C') == '°F'
|
||||||
else UnitOfTemperature.CELSIUS)
|
else UnitOfTemperature.CELSIUS)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self):
|
def current_temperature(self):
|
||||||
return _num(self._rep(TEMP_CURRENT_HREF).get('temperature'))
|
v = _num(self._rep(TEMP_CURRENT_HREF).get('temperature'))
|
||||||
|
if v is None:
|
||||||
|
v = _num(self._temps_vs().get('x.com.samsung.da.current'))
|
||||||
|
return v
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self):
|
def target_temperature(self):
|
||||||
return _num(self._rep(TEMP_DESIRED_HREF).get('temperature'))
|
v = _num(self._rep(TEMP_DESIRED_HREF).get('temperature'))
|
||||||
|
if v is None:
|
||||||
|
v = _num(self._temps_vs().get('x.com.samsung.da.desired'))
|
||||||
|
return v
|
||||||
|
|
||||||
def _range(self) -> list | None:
|
def _range(self) -> list | None:
|
||||||
r = self._rep(TEMP_DESIRED_HREF).get('range')
|
r = self._rep(TEMP_DESIRED_HREF).get('range')
|
||||||
return r if (isinstance(r, (list, tuple)) and len(r) == 2) else None
|
if isinstance(r, (list, tuple)) and len(r) == 2:
|
||||||
|
return r
|
||||||
|
item = self._temps_vs()
|
||||||
|
lo = _num(item.get('x.com.samsung.da.minimum'))
|
||||||
|
hi = _num(item.get('x.com.samsung.da.maximum'))
|
||||||
|
return [lo, hi] if (lo is not None and hi is not None) else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def min_temp(self) -> float:
|
def min_temp(self) -> float:
|
||||||
@@ -203,7 +237,10 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature_step(self) -> float:
|
def target_temperature_step(self) -> float:
|
||||||
return _num(self._rep(TEMP_CONTROL_HREF).get('increment')) or 1.0
|
return (_num(self._rep(TEMP_CONTROL_HREF).get('increment'))
|
||||||
|
or _num(self._rep(TEMP_CONTROL_HREF).get('x.com.samsung.da.increment'))
|
||||||
|
or _num(self._temps_vs().get('x.com.samsung.da.increment'))
|
||||||
|
or 1.0)
|
||||||
|
|
||||||
# -- hvac mode ----------------------------------------------------------
|
# -- hvac mode ----------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import cbor2
|
|||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers import issue_registry as ir
|
from homeassistant.helpers import issue_registry as ir
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
@@ -21,6 +22,7 @@ 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
|
from .registry.by_type import for_device, for_device_by_model
|
||||||
|
from .registry.capabilities.common import remote_control_enabled
|
||||||
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
|
||||||
@@ -36,6 +38,12 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
_SEED_PATH = ['device', '0']
|
_SEED_PATH = ['device', '0']
|
||||||
|
|
||||||
|
_REMOTE_CONTROL_DISABLED_MESSAGE = (
|
||||||
|
"Remote control is turned off on this device. Check your appliance's "
|
||||||
|
"manual for how to enable remote control before Home Assistant can "
|
||||||
|
"control it."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class _NoOpDescriptor:
|
class _NoOpDescriptor:
|
||||||
"""StateCache requires a descriptor with an on_observation hook. This
|
"""StateCache requires a descriptor with an on_observation hook. This
|
||||||
@@ -509,24 +517,49 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
|
|
||||||
async def async_send_command(self, bound_entity: BoundEntity,
|
async def async_send_command(self, bound_entity: BoundEntity,
|
||||||
payload: Any) -> None:
|
payload: Any) -> None:
|
||||||
"""Write a value to the device. Fire-and-forget style."""
|
"""Write a value to the device. Fire-and-forget style.
|
||||||
|
|
||||||
|
A description-level validate_fn (currently SwitchDesc only) runs
|
||||||
|
here rather than per-platform, so rejecting a write with a
|
||||||
|
user-facing message -- as opposed to write_fn's silent no-op below
|
||||||
|
-- is available to every platform for free. The remote-control
|
||||||
|
check runs first and applies to every platform unconditionally,
|
||||||
|
ahead of any description-specific validate_fn."""
|
||||||
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:
|
||||||
return
|
return
|
||||||
href = bound_entity.href
|
href = bound_entity.href
|
||||||
rep = self._cache.get(href or '') or {}
|
rep = self._cache.get(href or '') or {}
|
||||||
|
resources = self._cache.snapshot()
|
||||||
|
if not remote_control_enabled(resources):
|
||||||
|
raise ServiceValidationError(_REMOTE_CONTROL_DISABLED_MESSAGE)
|
||||||
|
validate_fn = getattr(desc, 'validate_fn', None)
|
||||||
|
if validate_fn is not None:
|
||||||
|
error = validate_fn(payload, rep, resources)
|
||||||
|
if error:
|
||||||
|
raise ServiceValidationError(error)
|
||||||
result = write_fn(payload, rep, href)
|
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
|
||||||
path_segs, body = result
|
path_segs, body = result
|
||||||
|
|
||||||
|
# Apply the write optimistically before starting the settle guard,
|
||||||
|
# not after -- mark_write_pending gates every source (poll, sweep,
|
||||||
|
# observe) through the same apply(), itself included, so flipping
|
||||||
|
# this order would have the guard drop the one update it exists to
|
||||||
|
# protect. Without an optimistic value in the cache for it to hold
|
||||||
|
# onto, the settle window was just delaying the real device
|
||||||
|
# confirmation for a few seconds on every write, which read exactly
|
||||||
|
# like the write being silently reverted (issue #27).
|
||||||
|
self._observe.apply(href, body, source='optimistic')
|
||||||
|
self._observe.mark_write_pending(href)
|
||||||
|
|
||||||
def _do_put():
|
def _do_put():
|
||||||
sess = self._session
|
sess = self._session
|
||||||
if sess is None:
|
if sess is None:
|
||||||
raise RuntimeError("no session")
|
raise RuntimeError("no session")
|
||||||
self._observe.mark_write_pending(href)
|
|
||||||
code, _ = sess.post(path_segs, cbor2.dumps(body), timeout=8.0)
|
code, _ = sess.post(path_segs, cbor2.dumps(body), timeout=8.0)
|
||||||
self._log.info("PUT %s → code %#04x", href, code)
|
self._log.info("PUT %s → code %#04x", href, code)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
|
|
||||||
from .registry.adapter import _key
|
from .registry.adapter import _key
|
||||||
from .registry.discovery import BoundEntity
|
from .registry.discovery import BoundEntity, _snake_to_title
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import LocalThingsCoordinator
|
from .coordinator import LocalThingsCoordinator
|
||||||
@@ -44,7 +44,7 @@ def _derive_name(state_key: str) -> str:
|
|||||||
instance number with a space: "door_cooler_open1" → "Door Cooler Open 1".
|
instance number with a space: "door_cooler_open1" → "Door Cooler Open 1".
|
||||||
"""
|
"""
|
||||||
name = re.sub(r'(\d+)$', lambda m: f' {m.group()}' if int(m.group()) > 0 else '', state_key)
|
name = re.sub(r'(\d+)$', lambda m: f' {m.group()}' if int(m.group()) > 0 else '', state_key)
|
||||||
return name.replace('_', ' ').title().strip()
|
return _snake_to_title(name).strip()
|
||||||
|
|
||||||
|
|
||||||
class LocalThingsEntity(CoordinatorEntity[LocalThingsCoordinator]):
|
class LocalThingsEntity(CoordinatorEntity[LocalThingsCoordinator]):
|
||||||
@@ -57,7 +57,15 @@ class LocalThingsEntity(CoordinatorEntity[LocalThingsCoordinator]):
|
|||||||
self._bound = bound
|
self._bound = bound
|
||||||
self._state_key = _key(bound)
|
self._state_key = _key(bound)
|
||||||
self._attr_unique_id = f"{DOMAIN}_{coordinator.device_serial}_{self._state_key}"
|
self._attr_unique_id = f"{DOMAIN}_{coordinator.device_serial}_{self._state_key}"
|
||||||
self._attr_name = bound.desc.name if bound.desc.name is not None else _derive_name(self._state_key)
|
if bound.desc.name is not None:
|
||||||
|
self._attr_name = bound.desc.name
|
||||||
|
elif bound.instance_name:
|
||||||
|
# A device-given instance name (e.g. an ice maker's "Cubed
|
||||||
|
# Ice") takes the place of the href-derived instance label,
|
||||||
|
# keeping the same entity-specific suffix (issue #27).
|
||||||
|
self._attr_name = f"{bound.instance_name} {_derive_name(bound.desc.key)}".strip()
|
||||||
|
else:
|
||||||
|
self._attr_name = _derive_name(self._state_key)
|
||||||
self._attr_translation_key = bound.desc.translation_key
|
self._attr_translation_key = bound.desc.translation_key
|
||||||
self._attr_icon = bound.desc.icon
|
self._attr_icon = bound.desc.icon
|
||||||
raw_cat = bound.desc.entity_category
|
raw_cat = bound.desc.entity_category
|
||||||
|
|||||||
@@ -12,5 +12,5 @@
|
|||||||
"pyOpenSSL>=23.0",
|
"pyOpenSSL>=23.0",
|
||||||
"smartthings-local>=0.1.0"
|
"smartthings-local>=0.1.0"
|
||||||
],
|
],
|
||||||
"version": "0.7.0"
|
"version": "0.10.1"
|
||||||
}
|
}
|
||||||
@@ -58,6 +58,9 @@ class LocalThingsNumber(LocalThingsEntity, NumberEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def native_min_value(self) -> float:
|
def native_min_value(self) -> float:
|
||||||
|
desc: NumberDesc = self._bound.desc
|
||||||
|
if desc.native_min_fn is not None:
|
||||||
|
return desc.native_min_fn(self.coordinator.resource(self._bound.href))
|
||||||
r = self._range_from_resource()
|
r = self._range_from_resource()
|
||||||
if r is not None:
|
if r is not None:
|
||||||
return float(r[0])
|
return float(r[0])
|
||||||
@@ -67,6 +70,9 @@ class LocalThingsNumber(LocalThingsEntity, NumberEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def native_max_value(self) -> float:
|
def native_max_value(self) -> float:
|
||||||
|
desc: NumberDesc = self._bound.desc
|
||||||
|
if desc.native_max_fn is not None:
|
||||||
|
return desc.native_max_fn(self.coordinator.resource(self._bound.href))
|
||||||
r = self._range_from_resource()
|
r = self._range_from_resource()
|
||||||
if r is not None:
|
if r is not None:
|
||||||
return float(r[1])
|
return float(r[1])
|
||||||
@@ -74,6 +80,15 @@ class LocalThingsNumber(LocalThingsEntity, NumberEntity):
|
|||||||
return self._attr_native_max_value
|
return self._attr_native_max_value
|
||||||
return super().native_max_value
|
return super().native_max_value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_step(self) -> float:
|
||||||
|
desc: NumberDesc = self._bound.desc
|
||||||
|
if desc.step_fn is not None:
|
||||||
|
return desc.step_fn(self.coordinator.resource(self._bound.href))
|
||||||
|
if hasattr(self, '_attr_native_step'):
|
||||||
|
return self._attr_native_step
|
||||||
|
return super().native_step
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self):
|
def native_value(self):
|
||||||
return (self.coordinator.data or {}).get(self._state_key)
|
return (self.coordinator.data or {}).get(self._state_key)
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class ObserveManager:
|
|||||||
self.last_mode_change_wall = time.time()
|
self.last_mode_change_wall = time.time()
|
||||||
self._settle_until: dict[str, float] = {}
|
self._settle_until: dict[str, float] = {}
|
||||||
self._settle_lock = threading.Lock()
|
self._settle_lock = threading.Lock()
|
||||||
|
self._cache_lock = threading.Lock()
|
||||||
self.subscribed_hrefs: set[str] = set()
|
self.subscribed_hrefs: set[str] = set()
|
||||||
self._notified: set[str] = set()
|
self._notified: set[str] = set()
|
||||||
self._last_notify_ts: float | None = None
|
self._last_notify_ts: float | None = None
|
||||||
@@ -92,11 +93,35 @@ class ObserveManager:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def apply(self, href: str, rep: dict, source: str) -> bool:
|
def apply(self, href: str, rep: dict, source: str) -> bool:
|
||||||
"""Gate a StateCache.apply_rep call through the write-settle guard."""
|
"""Gate a StateCache.apply_rep call through the write-settle guard.
|
||||||
|
|
||||||
|
Merges the incoming rep onto whatever's already cached for this
|
||||||
|
href rather than handing it to StateCache.apply_rep verbatim --
|
||||||
|
apply_rep does a full replace, and Samsung devices don't always
|
||||||
|
repeat every field on every update (issue #27: a /mode/vs/0
|
||||||
|
OBSERVE notify -- and, on at least one Bespoke fridge, even the
|
||||||
|
/device/0 sweep entry for that href -- can carry just `modes`,
|
||||||
|
omitting `supportedOptions`/`supportedModes` entirely). A full
|
||||||
|
replace would silently wipe fields a select entity's
|
||||||
|
exists_fn/options_field gates on the moment one partial update
|
||||||
|
comes through, even though nothing about the device's actual
|
||||||
|
supported options changed.
|
||||||
|
|
||||||
|
`apply()` is the sole path StateCache mutations flow through in
|
||||||
|
this component (poll, sweep, and OBSERVE notify all funnel here),
|
||||||
|
so `_cache_lock` serializes the read-then-write across those
|
||||||
|
threads (DTLS reader for notifies, executor threads for poll/
|
||||||
|
sweep) -- without it, two concurrent updates for the same href
|
||||||
|
could each read the same prior rep and the second writer would
|
||||||
|
silently lose the first's fields, reintroducing the exact bug
|
||||||
|
this merge fixes.
|
||||||
|
"""
|
||||||
if self._is_settling(href):
|
if self._is_settling(href):
|
||||||
self.log.debug("dropping %s update for %s (settling)", source, href)
|
self.log.debug("dropping %s update for %s (settling)", source, href)
|
||||||
return False
|
return False
|
||||||
return self.cache.apply_rep(href, rep, source=source)
|
with self._cache_lock:
|
||||||
|
merged = {**(self.cache.get(href) or {}), **rep}
|
||||||
|
return self.cache.apply_rep(href, merged, source=source)
|
||||||
|
|
||||||
def on_notification(self, href: str, payload: bytes) -> None:
|
def on_notification(self, href: str, payload: bytes) -> None:
|
||||||
"""Wired as DtlsCoapSession.on_notification. Runs on the DTLS
|
"""Wired as DtlsCoapSession.on_notification. Runs on the DTLS
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ._base import DeviceRegistry
|
from ._base import DeviceRegistry
|
||||||
from . import airconditioner, dishwasher, dryer, oven, refrigerator, washer
|
from . import airconditioner, dishwasher, dryer, oven, range as _range, refrigerator, washer
|
||||||
|
|
||||||
__all__ = ['DeviceRegistry', '_type_key', 'for_device', 'for_device_by_model']
|
__all__ = ['DeviceRegistry', '_type_key', 'for_device', 'for_device_by_model']
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ _REGISTRY_BY_KEY: dict[str, DeviceRegistry] = {
|
|||||||
'dishwasher': dishwasher.REGISTRY,
|
'dishwasher': dishwasher.REGISTRY,
|
||||||
'dryer': dryer.REGISTRY,
|
'dryer': dryer.REGISTRY,
|
||||||
'oven': oven.REGISTRY,
|
'oven': oven.REGISTRY,
|
||||||
|
'range': _range.REGISTRY,
|
||||||
'refrigerator': refrigerator.REGISTRY,
|
'refrigerator': refrigerator.REGISTRY,
|
||||||
'washer': washer.REGISTRY,
|
'washer': washer.REGISTRY,
|
||||||
}
|
}
|
||||||
@@ -68,6 +69,8 @@ def for_device(one_ui_version: str) -> Optional[DeviceRegistry]:
|
|||||||
_CONSUMER_PREFIX_TO_KEY: dict[str, str] = {
|
_CONSUMER_PREFIX_TO_KEY: dict[str, str] = {
|
||||||
'WW': 'washer',
|
'WW': 'washer',
|
||||||
'WD': 'washer',
|
'WD': 'washer',
|
||||||
|
'WF': 'washer',
|
||||||
|
'WV': 'washer', # FlexWash twin units (e.g. WV55M9600AW) -- issue #19
|
||||||
'DV': 'dryer',
|
'DV': 'dryer',
|
||||||
'DW': 'dishwasher',
|
'DW': 'dishwasher',
|
||||||
}
|
}
|
||||||
@@ -94,4 +97,14 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
|||||||
# a modelNum carrying the '_PRAC_' (Package Room Air Conditioner) token.
|
# a modelNum carrying the '_PRAC_' (Package Room Air Conditioner) token.
|
||||||
if key is None and '_PRAC_' in (model_num or ''):
|
if key is None and '_PRAC_' in (model_num or ''):
|
||||||
key = 'airconditioner'
|
key = 'airconditioner'
|
||||||
|
# Older/simpler RAC boards (e.g. TP2X_RAC_20K, issue #37) use the plain
|
||||||
|
# '_RAC_' token instead -- distinct from '_PRAC_' above (no overlap: the
|
||||||
|
# 'P' sits between the underscore and 'RAC' in that token).
|
||||||
|
if key is None and '_RAC_' in (model_num or ''):
|
||||||
|
key = 'airconditioner'
|
||||||
|
# Range/cooktop-oven combos (e.g. TP1X_DA-KS-RANGE-0102X, issue #44) --
|
||||||
|
# like the RAC/PRAC air conditioners above, these report no oneUiVersion
|
||||||
|
# and don't match the washer/dryer/dishwasher consumer-prefix map either.
|
||||||
|
if key is None and '-RANGE-' in (model_num or '').upper():
|
||||||
|
key = 'range'
|
||||||
return _REGISTRY_BY_KEY.get(key) if key else None
|
return _REGISTRY_BY_KEY.get(key) if key else None
|
||||||
@@ -3,27 +3,28 @@
|
|||||||
The first device whose core controls surface as a single composite HA `climate`
|
The first device whose core controls surface as a single composite HA `climate`
|
||||||
entity (see capabilities/airconditioner.py and climate.py). Power/mode/temp/wind
|
entity (see capabilities/airconditioner.py and climate.py). Power/mode/temp/wind
|
||||||
are consumed by that entity rather than exposed as separate switches/selects, so
|
are consumed by that entity rather than exposed as separate switches/selects, so
|
||||||
this registry deliberately does NOT include the common POWER caps -- on/off is
|
this registry includes *common.UNIVERSAL but deliberately NOT common.POWER --
|
||||||
the climate entity's HVACMode.OFF / TURN_ON/OFF.
|
on/off is the climate entity's HVACMode.OFF / TURN_ON/OFF. See common.POWER's
|
||||||
|
own comment in capabilities/common.py for why it's excluded.
|
||||||
|
|
||||||
Reuses common.ALARMS + common.ENERGY_METER, fridge.FIRMWARE_UPDATE (as every
|
Reuses dishwasher.DIAGNOSIS for /diagnosis/vs/0.
|
||||||
registry does), and dishwasher.DIAGNOSIS for /diagnosis/vs/0.
|
|
||||||
"""
|
"""
|
||||||
from ..capabilities import airconditioner, common, dishwasher, fridge, ignored
|
from ..capabilities import airconditioner, common, dishwasher, ignored
|
||||||
from ._base import DeviceRegistry, _build
|
from ._base import DeviceRegistry, _build
|
||||||
|
|
||||||
REGISTRY = DeviceRegistry(
|
REGISTRY = DeviceRegistry(
|
||||||
name='airconditioner',
|
name='airconditioner',
|
||||||
capabilities=_build([
|
capabilities=_build([
|
||||||
*ignored.IGNORED,
|
*ignored.IGNORED,
|
||||||
common.ALARMS,
|
*common.UNIVERSAL,
|
||||||
common.ENERGY_METER,
|
|
||||||
fridge.FIRMWARE_UPDATE,
|
|
||||||
dishwasher.DIAGNOSIS,
|
dishwasher.DIAGNOSIS,
|
||||||
airconditioner.CLIMATE,
|
airconditioner.CLIMATE,
|
||||||
airconditioner.AIR_PURIFY,
|
airconditioner.AIR_PURIFY,
|
||||||
airconditioner.AUTO_CLEAN,
|
airconditioner.AUTO_CLEAN,
|
||||||
airconditioner.AIR_FILTER,
|
airconditioner.AIR_FILTER,
|
||||||
|
airconditioner.DISPLAY_LIGHT,
|
||||||
|
airconditioner.MUTE_ONCE,
|
||||||
|
airconditioner.CURRENT_LIMIT,
|
||||||
*airconditioner.COVERAGE,
|
*airconditioner.COVERAGE,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
"""Dishwasher device registry."""
|
"""Dishwasher device registry."""
|
||||||
from ..capabilities import common, dishwasher, fridge, ignored, laundry, operational
|
from ..capabilities import common, dishwasher, ignored, laundry, operational
|
||||||
from ._base import DeviceRegistry, _build
|
from ._base import DeviceRegistry, _build
|
||||||
|
|
||||||
REGISTRY = DeviceRegistry(
|
REGISTRY = DeviceRegistry(
|
||||||
name='dishwasher',
|
name='dishwasher',
|
||||||
capabilities=_build([
|
capabilities=_build([
|
||||||
*ignored.IGNORED,
|
*ignored.IGNORED,
|
||||||
common.POWER_GENERIC,
|
*common.UNIVERSAL,
|
||||||
common.POWER_VS_FALLBACK,
|
*common.POWER,
|
||||||
common.KIDS_LOCK_GENERIC,
|
|
||||||
common.KIDS_LOCK_VS_FALLBACK,
|
|
||||||
common.REMOTE_CONTROL_GENERIC,
|
|
||||||
common.REMOTE_CONTROL_VS_FALLBACK,
|
|
||||||
common.ALARMS,
|
|
||||||
common.ENERGY_METER,
|
|
||||||
common.WATER_METER,
|
common.WATER_METER,
|
||||||
common.WATER_FILTER,
|
common.WATER_FILTER,
|
||||||
operational.OPERATIONAL_STATE,
|
operational.OPERATIONAL_STATE,
|
||||||
@@ -25,6 +19,5 @@ REGISTRY = DeviceRegistry(
|
|||||||
laundry.DOOR_LED,
|
laundry.DOOR_LED,
|
||||||
laundry.SOUND_MODE,
|
laundry.SOUND_MODE,
|
||||||
laundry.SOUND_VOLUME,
|
laundry.SOUND_VOLUME,
|
||||||
fridge.FIRMWARE_UPDATE,
|
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -9,21 +9,15 @@ objects the washer registry uses -- washer and dryer expose the same
|
|||||||
DA_WM_-family surface, so they stay consistent instead of each carrying a
|
DA_WM_-family surface, so they stay consistent instead of each carrying a
|
||||||
bespoke variant.
|
bespoke variant.
|
||||||
"""
|
"""
|
||||||
from ..capabilities import common, dryer, fridge, ignored, laundry, operational
|
from ..capabilities import common, dryer, ignored, laundry, operational
|
||||||
from ._base import DeviceRegistry, _build
|
from ._base import DeviceRegistry, _build
|
||||||
|
|
||||||
REGISTRY = DeviceRegistry(
|
REGISTRY = DeviceRegistry(
|
||||||
name='dryer',
|
name='dryer',
|
||||||
capabilities=_build([
|
capabilities=_build([
|
||||||
*ignored.IGNORED,
|
*ignored.IGNORED,
|
||||||
common.POWER_GENERIC,
|
*common.UNIVERSAL,
|
||||||
common.POWER_VS_FALLBACK,
|
*common.POWER,
|
||||||
common.KIDS_LOCK_GENERIC,
|
|
||||||
common.KIDS_LOCK_VS_FALLBACK,
|
|
||||||
common.REMOTE_CONTROL_GENERIC,
|
|
||||||
common.REMOTE_CONTROL_VS_FALLBACK,
|
|
||||||
common.ALARMS,
|
|
||||||
common.ENERGY_METER,
|
|
||||||
operational.OPERATIONAL_STATE,
|
operational.OPERATIONAL_STATE,
|
||||||
laundry.DOOR_LED,
|
laundry.DOOR_LED,
|
||||||
laundry.SOUND_MODE,
|
laundry.SOUND_MODE,
|
||||||
@@ -32,6 +26,5 @@ REGISTRY = DeviceRegistry(
|
|||||||
dryer.DRYER_SETTINGS,
|
dryer.DRYER_SETTINGS,
|
||||||
dryer.DRYER_COURSE,
|
dryer.DRYER_COURSE,
|
||||||
dryer.DRYER_DIAGNOSIS,
|
dryer.DRYER_DIAGNOSIS,
|
||||||
fridge.FIRMWARE_UPDATE,
|
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -1,24 +1,19 @@
|
|||||||
"""Oven device registry."""
|
"""Oven device registry."""
|
||||||
from ..capabilities import common, fridge, ignored, oven
|
from ..capabilities import common, ignored, oven
|
||||||
from ._base import DeviceRegistry, _build
|
from ._base import DeviceRegistry, _build
|
||||||
|
|
||||||
REGISTRY = DeviceRegistry(
|
REGISTRY = DeviceRegistry(
|
||||||
name='oven',
|
name='oven',
|
||||||
capabilities=_build([
|
capabilities=_build([
|
||||||
*ignored.IGNORED,
|
*ignored.IGNORED,
|
||||||
common.POWER_GENERIC,
|
*common.UNIVERSAL,
|
||||||
common.POWER_VS_FALLBACK,
|
*common.POWER,
|
||||||
common.KIDS_LOCK_GENERIC,
|
|
||||||
common.KIDS_LOCK_VS_FALLBACK,
|
|
||||||
common.REMOTE_CONTROL_GENERIC,
|
|
||||||
common.REMOTE_CONTROL_VS_FALLBACK,
|
|
||||||
common.ALARMS,
|
|
||||||
oven.OVEN_CAVITY,
|
oven.OVEN_CAVITY,
|
||||||
oven.OVEN_SETPOINT,
|
oven.OVEN_SETPOINT,
|
||||||
oven.OVEN_MODE,
|
oven.OVEN_MODE,
|
||||||
oven.OVEN_OPERATIONAL_STATE,
|
oven.OVEN_OPERATIONAL_STATE,
|
||||||
oven.OVEN_DOOR,
|
oven.OVEN_DOOR,
|
||||||
oven.OVEN_CONNECTED,
|
oven.OVEN_CONNECTED,
|
||||||
fridge.FIRMWARE_UPDATE,
|
oven.OVEN_SPEC,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Range (oven + cooktop combo) device registry — issue #44.
|
||||||
|
|
||||||
|
Reuses the oven family's cavity/setpoint/mode/operational-state/door/
|
||||||
|
connected capabilities wholesale (a range's oven half is the same OCF
|
||||||
|
surface as a standalone oven) and adds the cooktop-specific capabilities
|
||||||
|
for the burner half.
|
||||||
|
"""
|
||||||
|
from ..capabilities import common, ignored, oven
|
||||||
|
from ..capabilities import range as range_caps
|
||||||
|
from ._base import DeviceRegistry, _build
|
||||||
|
|
||||||
|
REGISTRY = DeviceRegistry(
|
||||||
|
name='range',
|
||||||
|
capabilities=_build([
|
||||||
|
*ignored.IGNORED,
|
||||||
|
*common.UNIVERSAL,
|
||||||
|
*common.POWER,
|
||||||
|
oven.OVEN_CAVITY,
|
||||||
|
oven.OVEN_SETPOINT,
|
||||||
|
oven.OVEN_MODE,
|
||||||
|
oven.OVEN_OPERATIONAL_STATE,
|
||||||
|
oven.OVEN_DOOR,
|
||||||
|
oven.OVEN_CONNECTED,
|
||||||
|
oven.OVEN_SPEC,
|
||||||
|
range_caps.COOKTOP_STATUS,
|
||||||
|
range_caps.COOKTOP_SPEC,
|
||||||
|
range_caps.COOKTOP_SAFETY,
|
||||||
|
]),
|
||||||
|
)
|
||||||
@@ -1,22 +1,17 @@
|
|||||||
"""Refrigerator device registry."""
|
"""Refrigerator device registry."""
|
||||||
from ..capabilities import common, fridge, ignored
|
from ..capabilities import common, dishwasher, fridge, ignored
|
||||||
from ._base import DeviceRegistry, _build
|
from ._base import DeviceRegistry, _build
|
||||||
|
|
||||||
REGISTRY = DeviceRegistry(
|
REGISTRY = DeviceRegistry(
|
||||||
name='refrigerator',
|
name='refrigerator',
|
||||||
capabilities=_build([
|
capabilities=_build([
|
||||||
*ignored.IGNORED,
|
*ignored.IGNORED,
|
||||||
common.POWER_GENERIC,
|
*common.UNIVERSAL,
|
||||||
common.POWER_VS_FALLBACK,
|
*common.POWER,
|
||||||
fridge.STATUS_LOCK,
|
fridge.STATUS_LOCK,
|
||||||
fridge.DOOR_ALERT,
|
fridge.DOOR_ALERT,
|
||||||
common.KIDS_LOCK_GENERIC,
|
|
||||||
common.KIDS_LOCK_VS_FALLBACK,
|
|
||||||
common.REMOTE_CONTROL_GENERIC,
|
|
||||||
common.REMOTE_CONTROL_VS_FALLBACK,
|
|
||||||
common.ALARMS,
|
|
||||||
common.ENERGY_METER,
|
|
||||||
common.WATER_FILTER,
|
common.WATER_FILTER,
|
||||||
|
dishwasher.DIAGNOSIS,
|
||||||
fridge.ICEMAKER_NIGHTTIME,
|
fridge.ICEMAKER_NIGHTTIME,
|
||||||
fridge.FLEX_ZONE,
|
fridge.FLEX_ZONE,
|
||||||
fridge.REFRIGERATION,
|
fridge.REFRIGERATION,
|
||||||
@@ -26,13 +21,14 @@ REGISTRY = DeviceRegistry(
|
|||||||
fridge.CABINET_LIGHT_ENHANCED,
|
fridge.CABINET_LIGHT_ENHANCED,
|
||||||
fridge.SABBATH,
|
fridge.SABBATH,
|
||||||
fridge.BEVERAGE_ZONE,
|
fridge.BEVERAGE_ZONE,
|
||||||
fridge.FIRMWARE_UPDATE,
|
fridge.PANTRY_ZONE,
|
||||||
fridge.DEFROST_DELAY,
|
fridge.DEFROST_DELAY,
|
||||||
|
fridge.DEFROST_DELAY_NATIVE_DUPLICATE,
|
||||||
fridge.DEFROST_BLOCK_STATUS,
|
fridge.DEFROST_BLOCK_STATUS,
|
||||||
fridge.SELF_CHECK,
|
|
||||||
fridge.DOORS_FALLBACK,
|
fridge.DOORS_FALLBACK,
|
||||||
fridge.TEMPERATURES_FALLBACK,
|
fridge.TEMPERATURES_FALLBACK,
|
||||||
fridge.ICEMAKER_STATUS_FALLBACK,
|
fridge.ICEMAKER_STATUS_FALLBACK,
|
||||||
|
fridge.ICEMAKER_STATUS_NATIVE_DUPLICATE,
|
||||||
fridge.REFRIGERATION_FALLBACK,
|
fridge.REFRIGERATION_FALLBACK,
|
||||||
]),
|
]),
|
||||||
pattern_capabilities=[
|
pattern_capabilities=[
|
||||||
|
|||||||
@@ -1,26 +1,19 @@
|
|||||||
"""Washer device registry."""
|
"""Washer device registry."""
|
||||||
from ..capabilities import common, dishwasher, fridge, ignored, laundry, operational, washer
|
from ..capabilities import common, dishwasher, ignored, laundry, operational, washer
|
||||||
from ._base import DeviceRegistry, _build
|
from ._base import DeviceRegistry, _build
|
||||||
|
|
||||||
REGISTRY = DeviceRegistry(
|
REGISTRY = DeviceRegistry(
|
||||||
name='washer',
|
name='washer',
|
||||||
capabilities=_build([
|
capabilities=_build([
|
||||||
*ignored.IGNORED,
|
*ignored.IGNORED,
|
||||||
common.POWER_GENERIC,
|
*common.UNIVERSAL,
|
||||||
common.POWER_VS_FALLBACK,
|
*common.POWER,
|
||||||
common.KIDS_LOCK_GENERIC,
|
|
||||||
common.KIDS_LOCK_VS_FALLBACK,
|
|
||||||
common.REMOTE_CONTROL_GENERIC,
|
|
||||||
common.REMOTE_CONTROL_VS_FALLBACK,
|
|
||||||
washer.WASHER_SETTINGS,
|
washer.WASHER_SETTINGS,
|
||||||
washer.WASHER_COURSE,
|
washer.WASHER_COURSE,
|
||||||
laundry.BUZZER_SOUND,
|
laundry.BUZZER_SOUND,
|
||||||
laundry.JOB_BEGINNING_STATUS,
|
laundry.JOB_BEGINNING_STATUS,
|
||||||
common.ALARMS,
|
|
||||||
common.ENERGY_METER,
|
|
||||||
common.WATER_METER,
|
common.WATER_METER,
|
||||||
operational.OPERATIONAL_STATE,
|
operational.OPERATIONAL_STATE,
|
||||||
dishwasher.DIAGNOSIS,
|
dishwasher.DIAGNOSIS,
|
||||||
fridge.FIRMWARE_UPDATE,
|
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -15,7 +15,7 @@ different schema (see capabilities/__init__.py). They live only in the AC
|
|||||||
by_type registry.
|
by_type registry.
|
||||||
"""
|
"""
|
||||||
from ..capability import Capability
|
from ..capability import Capability
|
||||||
from ..entities import ClimateDesc, SensorDesc, SwitchDesc
|
from ..entities import BinarySensorDesc, ClimateDesc, SensorDesc, SwitchDesc
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Canonical AC resource hrefs. The climate entity (climate.py) binds the
|
# Canonical AC resource hrefs. The climate entity (climate.py) binds the
|
||||||
@@ -34,10 +34,12 @@ HREF_TEMP_CONTROL = '/temperature/control/vs/0' # target_temperature_step
|
|||||||
HREF_WIND_STRENGTH = '/wind/strength/vs/0' # fan_mode
|
HREF_WIND_STRENGTH = '/wind/strength/vs/0' # fan_mode
|
||||||
HREF_WIND_DIRECTION = '/wind/direction/vs/0' # swing_mode
|
HREF_WIND_DIRECTION = '/wind/direction/vs/0' # swing_mode
|
||||||
HREF_CONVENIENT = '/mode/convenient/vs/0' # preset_mode
|
HREF_CONVENIENT = '/mode/convenient/vs/0' # preset_mode
|
||||||
|
HREF_TEMPS_VS = '/temperatures/vs/0' # vendor temp fallback (items[] array)
|
||||||
|
|
||||||
CLIMATE_CONSUMED_HREFS = [
|
CLIMATE_CONSUMED_HREFS = [
|
||||||
HREF_POWER, HREF_POWER_VS, HREF_TEMP_CURRENT, HREF_TEMP_DESIRED,
|
HREF_POWER, HREF_POWER_VS, HREF_TEMP_CURRENT, HREF_TEMP_DESIRED,
|
||||||
HREF_TEMP_CONTROL, HREF_WIND_STRENGTH, HREF_WIND_DIRECTION, HREF_CONVENIENT,
|
HREF_TEMP_CONTROL, HREF_TEMPS_VS, HREF_WIND_STRENGTH, HREF_WIND_DIRECTION,
|
||||||
|
HREF_CONVENIENT,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -146,6 +148,57 @@ AIR_FILTER = Capability(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DISPLAY_LIGHT = Capability(
|
||||||
|
href='/light/vs/0',
|
||||||
|
poll_tier='cold',
|
||||||
|
entities=(
|
||||||
|
SwitchDesc(key='display_light', field='mode',
|
||||||
|
name='Display light', icon='mdi:led-on',
|
||||||
|
entity_category='config',
|
||||||
|
value_fn=lambda v: v == 'On',
|
||||||
|
write_fn=lambda p, rep, href=None: (
|
||||||
|
['light', 'vs', '0'],
|
||||||
|
{'mode': 'On' if p == 'On' else 'Off'})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Confirmed against issue #38's dump (TP1X_DA-AC-RAC-01001_0000): a single
|
||||||
|
# boolean field, no vendor prefix, mirroring the On/Off convention used
|
||||||
|
# throughout the rest of this API.
|
||||||
|
MUTE_ONCE = Capability(
|
||||||
|
href='/option/muteonce/vs/0',
|
||||||
|
poll_tier='warm',
|
||||||
|
entities=(
|
||||||
|
SwitchDesc(key='mute_once', field='muteonce',
|
||||||
|
name='Mute once', icon='mdi:volume-mute',
|
||||||
|
entity_category='config',
|
||||||
|
value_fn=lambda v: v == 'On',
|
||||||
|
write_fn=lambda p, rep, href=None: (
|
||||||
|
['option', 'muteonce', 'vs', '0'],
|
||||||
|
{'muteonce': 'On' if p == 'On' else 'Off'})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Circuit-breaker current-limit setting (issue #38, TP1X board): `operation`
|
||||||
|
# toggles the limiter and `modes` picks a level out of `supportedModes`
|
||||||
|
# (seen as '3'..'9'). No vendor field-name prefix and no unit/label in the
|
||||||
|
# dump to confirm what the levels mean (amps vs. an abstract tier) --
|
||||||
|
# exposed read-only per the 'don't guess' rule rather than risking an
|
||||||
|
# unverified write to live HVAC hardware.
|
||||||
|
CURRENT_LIMIT = Capability(
|
||||||
|
href='/electriccurrent/vs/0',
|
||||||
|
poll_tier='cold',
|
||||||
|
entities=(
|
||||||
|
BinarySensorDesc(key='current_limit_enabled', field='operation',
|
||||||
|
name='Current limit enabled', icon='mdi:current-ac',
|
||||||
|
entity_category='diagnostic',
|
||||||
|
value_fn=lambda v: v == 'On'),
|
||||||
|
SensorDesc(key='current_limit_level', field='modes',
|
||||||
|
name='Current limit level', icon='mdi:current-ac',
|
||||||
|
entity_category='diagnostic'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 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.
|
||||||
@@ -153,10 +206,18 @@ AIR_FILTER = Capability(
|
|||||||
# (/mode/vs/0 handled above, /temperatures/vs/0, /humidity/*) collide with
|
# (/mode/vs/0 handled above, /temperatures/vs/0, /humidity/*) collide with
|
||||||
# other families' schemas. A no-entity Capability still marks the href as
|
# other families' schemas. A no-entity Capability still marks the href as
|
||||||
# bound so discover() reports no coverage gap.
|
# bound so discover() reports no coverage gap.
|
||||||
|
#
|
||||||
|
# CLIMATE_CONSUMED_HREFS carry the climate card's actual displayed state
|
||||||
|
# (power, current/target temp, fan, swing, preset) -- the coordinator only
|
||||||
|
# OBSERVE-subscribes and sub-polls 'hot'/'warm' hrefs (see coordinator.py),
|
||||||
|
# so leaving these at the Capability default of 'cold' meant every state
|
||||||
|
# change was invisible until the next full /device/0 summary sweep
|
||||||
|
# (~30s -- issue #17: instant device response, 20-30s HA lag). Pin them to
|
||||||
|
# 'warm' -- same tier as CLIMATE's own primary href -- so they get push
|
||||||
|
# notifications (or, in poll-only mode, the warm sub-poll cadence) instead
|
||||||
|
# of waiting on the summary sweep.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
_AC_IGNORED = [
|
_AC_IGNORED = [
|
||||||
# Vendor superset that duplicates the OCF /temperature/current+desired pair.
|
|
||||||
'/temperatures/vs/0',
|
|
||||||
# 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',
|
||||||
@@ -164,7 +225,29 @@ _AC_IGNORED = [
|
|||||||
'/humidity/vs/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
|
||||||
|
# user-actionable state or no documented write contract, so per the
|
||||||
|
# 'don't guess' rule they are ignored rather than modeled.
|
||||||
|
# /option/muteonce/vs/0 and /selfcheck/vs/0 are deliberately NOT here --
|
||||||
|
# see MUTE_ONCE above and common.SELF_CHECK (via common.UNIVERSAL) in
|
||||||
|
# the by_type registry, both of which have a confirmed, cleanly
|
||||||
|
# modelable contract.
|
||||||
|
'/airlevelcheck/vs/0', # periodic air-quality sensing scheduler plumbing
|
||||||
|
'/aisleep/vs/0', # AI-sleep feedback state (no actionable control)
|
||||||
|
'/availablecontrolsets/vs/0', # opaque hex-encoded control-set bitmap
|
||||||
|
'/da/softreset/vs/0', # soft-reset trigger plumbing
|
||||||
|
'/keepnormalstate/vs/0', # internal keep-normal flag
|
||||||
|
'/mds/absencemonitoring/vs/0', # motion-detection sensor plumbing (empty here)
|
||||||
|
'/mds/absencestate/vs/0', # motion-detection state (empty here)
|
||||||
|
'/remotedatacontrol/vs/0', # remote data-control session status
|
||||||
|
'/remotetemperature/vs/0', # external temp-sensor feed (unset on this unit)
|
||||||
|
'/reserverulesets/vs/0', # opaque hex-encoded schedule reservation blob
|
||||||
|
'/welcome/temperature/vs/0', # welcome-cooling plumbing
|
||||||
]
|
]
|
||||||
|
|
||||||
# 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).
|
||||||
COVERAGE = [Capability(href=h) for h in (CLIMATE_CONSUMED_HREFS + _AC_IGNORED)]
|
COVERAGE = [
|
||||||
|
Capability(href=h, poll_tier='warm') for h in CLIMATE_CONSUMED_HREFS
|
||||||
|
] + [
|
||||||
|
Capability(href=h) for h in _AC_IGNORED
|
||||||
|
]
|
||||||
@@ -11,7 +11,9 @@ against live device dumps:
|
|||||||
/filter/waterfilter/vs/0 -> x.com.samsung.da.filterUsage / filterStatus
|
/filter/waterfilter/vs/0 -> x.com.samsung.da.filterUsage / filterStatus
|
||||||
"""
|
"""
|
||||||
from ..capability import Capability
|
from ..capability import Capability
|
||||||
from ..entities import BinarySensorDesc, SensorDesc, SwitchDesc
|
from ..entities import (
|
||||||
|
BinarySensorDesc, ButtonDesc, SelectDesc, SensorDesc, SwitchDesc,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _num(v):
|
def _num(v):
|
||||||
@@ -117,8 +119,30 @@ KIDS_LOCK_VS_FALLBACK = Capability(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remote_control_enabled(resources: dict) -> bool:
|
||||||
|
"""Single source of truth for the /remotectrl on/off signal, mirroring
|
||||||
|
REMOTE_CONTROL_GENERIC/_VS_FALLBACK's href/field pair and precedence
|
||||||
|
below. Used both to render the read-only Smart Control binary_sensor
|
||||||
|
(via those two descriptors) and, from coordinator.async_send_command,
|
||||||
|
to block writes outright when remote control is off. Both hrefs are
|
||||||
|
poll_tier='warm' below so that gate reads recent state (subscribed
|
||||||
|
when observe is live, subpolled every ~6s otherwise) rather than a
|
||||||
|
once-per-30s cold summary poll. True (assume enabled) when neither
|
||||||
|
href is present -- most device types don't report this capability
|
||||||
|
at all."""
|
||||||
|
generic = resources.get('/remotectrl/0')
|
||||||
|
if generic is not None:
|
||||||
|
return bool(generic.get('value'))
|
||||||
|
fallback = resources.get('/remotectrl/vs/0')
|
||||||
|
if fallback is not None:
|
||||||
|
return str(fallback.get('x.com.samsung.da.remoteControlEnabled')).lower() == 'true'
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
REMOTE_CONTROL_GENERIC = Capability(
|
REMOTE_CONTROL_GENERIC = Capability(
|
||||||
href='/remotectrl/0',
|
href='/remotectrl/0',
|
||||||
|
poll_tier='warm',
|
||||||
entities=(
|
entities=(
|
||||||
BinarySensorDesc(key='remote_control', field='value',
|
BinarySensorDesc(key='remote_control', field='value',
|
||||||
name='Smart Control', device_class='connectivity',
|
name='Smart Control', device_class='connectivity',
|
||||||
@@ -129,6 +153,7 @@ REMOTE_CONTROL_GENERIC = Capability(
|
|||||||
REMOTE_CONTROL_VS_FALLBACK = Capability(
|
REMOTE_CONTROL_VS_FALLBACK = Capability(
|
||||||
href='/remotectrl/vs/0',
|
href='/remotectrl/vs/0',
|
||||||
match_fn=lambda rep, resources: '/remotectrl/0' not in resources,
|
match_fn=lambda rep, resources: '/remotectrl/0' not in resources,
|
||||||
|
poll_tier='warm',
|
||||||
entities=(
|
entities=(
|
||||||
BinarySensorDesc(key='remote_control',
|
BinarySensorDesc(key='remote_control',
|
||||||
field='x.com.samsung.da.remoteControlEnabled',
|
field='x.com.samsung.da.remoteControlEnabled',
|
||||||
@@ -175,6 +200,38 @@ ENERGY_METER = Capability(
|
|||||||
state_class='total_increasing', unit='kWh', value_fn=wh_to_kwh,
|
state_class='total_increasing', unit='kWh', value_fn=wh_to_kwh,
|
||||||
exists_fn=lambda rep, resources: (
|
exists_fn=lambda rep, resources: (
|
||||||
not rep or 'x.com.samsung.da.cumulativePower' in rep)),
|
not rep or 'x.com.samsung.da.cumulativePower' in rep)),
|
||||||
|
# cumulativeConsumption is a second, independently-varying running
|
||||||
|
# total alongside cumulativePower -- some fridges (issue #26) report
|
||||||
|
# both. Self-gates off where only cumulativePower is present. `not
|
||||||
|
# rep or` keeps the same empty-{} stub carve-out as power_watts/
|
||||||
|
# energy_kwh above -- without it, an exists_fn permanently drops the
|
||||||
|
# entity if setup happens to land on a not-yet-fetched stub.
|
||||||
|
SensorDesc(key='power_energy_kwh', field='x.com.samsung.da.cumulativeConsumption',
|
||||||
|
name='Power energy', device_class='energy',
|
||||||
|
state_class='total_increasing', unit='kWh', value_fn=wh_to_kwh,
|
||||||
|
exists_fn=lambda rep, resources: (
|
||||||
|
not rep or 'x.com.samsung.da.cumulativeConsumption' in rep)),
|
||||||
|
# AI Energy Mode's lifetime savings estimate vs. an unoptimized
|
||||||
|
# baseline -- present on some models (e.g. TP1X_REF_21K, issue #21/
|
||||||
|
# #27) and absent on others (issue #20/#26), unlike cumulativePower.
|
||||||
|
SensorDesc(key='energy_saved_kwh', field='x.com.samsung.da.cumulativeSavedPower',
|
||||||
|
name='Energy saved', device_class='energy',
|
||||||
|
state_class='total_increasing', unit='kWh', value_fn=wh_to_kwh,
|
||||||
|
exists_fn=lambda rep, resources: (
|
||||||
|
not rep or 'x.com.samsung.da.cumulativeSavedPower' in rep)),
|
||||||
|
# Monthly billing-cycle totals -- the completed prior month and the
|
||||||
|
# in-progress current month. Not ever-increasing (each resets at
|
||||||
|
# month boundary), so no state_class.
|
||||||
|
SensorDesc(key='energy_last_month_kwh', field='x.com.samsung.da.monthlyConsumption',
|
||||||
|
name='Energy (last month)', device_class='energy',
|
||||||
|
unit='kWh', value_fn=wh_to_kwh,
|
||||||
|
exists_fn=lambda rep, resources: (
|
||||||
|
not rep or 'x.com.samsung.da.monthlyConsumption' in rep)),
|
||||||
|
SensorDesc(key='energy_this_month_kwh', field='x.com.samsung.da.thismonthlyConsumption',
|
||||||
|
name='Energy (this month)', device_class='energy',
|
||||||
|
unit='kWh', value_fn=wh_to_kwh,
|
||||||
|
exists_fn=lambda rep, resources: (
|
||||||
|
not rep or 'x.com.samsung.da.thismonthlyConsumption' in rep)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -199,3 +256,162 @@ WATER_FILTER = Capability(
|
|||||||
name='Filter status', icon='mdi:filter-check'),
|
name='Filter status', icon='mdi:filter-check'),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# AI energy-saving level -- '0' is off, and supportedAiLevel lists the
|
||||||
|
# additional level(s) the device offers ('1' meaning just "on" on most
|
||||||
|
# hardware, but multi-level boards have been reported). Verified cross-family:
|
||||||
|
# fridge (issue #21) and washer (issue #40) both expose this href.
|
||||||
|
#
|
||||||
|
# supportedAiLevel is a single-entry list on most captured hardware, where a
|
||||||
|
# select would offer only one real choice against an implicit "off" -- shown
|
||||||
|
# as a switch instead. '0' itself is never in supportedAiLevel but has been
|
||||||
|
# observed live as the off value of aiLevel, so the select synthesizes it
|
||||||
|
# back in as an explicit option rather than leaving no way to turn off.
|
||||||
|
#
|
||||||
|
# No translation_key: aiLevel's values are plain digit strings, and
|
||||||
|
# select.py's _display() already renders an untranslated numeric string
|
||||||
|
# as-is -- there's nothing a strings.json entry adds that's worth maintaining
|
||||||
|
# against an unknown, growing number of future levels.
|
||||||
|
|
||||||
|
|
||||||
|
def _ai_energy_supported_levels(rep):
|
||||||
|
"""supportedAiLevel as a list -- a stray scalar (e.g. a string) must not
|
||||||
|
be len()-checked as if it were a list."""
|
||||||
|
sl = rep.get('supportedAiLevel')
|
||||||
|
return list(sl) if isinstance(sl, (list, tuple)) else []
|
||||||
|
|
||||||
|
|
||||||
|
def _ai_energy_level_options(resources):
|
||||||
|
rep = resources.get('/energy/ailevel/vs/0') or {}
|
||||||
|
return ['0', *_ai_energy_supported_levels(rep)]
|
||||||
|
|
||||||
|
|
||||||
|
def _ai_energy_level_write(p, rep, href=None):
|
||||||
|
return ['energy', 'ailevel', 'vs', '0'], {'aiLevel': p}
|
||||||
|
|
||||||
|
|
||||||
|
def _ai_energy_level_switch_write(p, rep, href=None):
|
||||||
|
levels = _ai_energy_supported_levels(rep)
|
||||||
|
on_level = levels[0] if levels else '1'
|
||||||
|
return ['energy', 'ailevel', 'vs', '0'], {'aiLevel': on_level if p == 'On' else '0'}
|
||||||
|
|
||||||
|
|
||||||
|
AI_ENERGY_LEVEL = Capability(
|
||||||
|
href='/energy/ailevel/vs/0',
|
||||||
|
poll_tier='cold',
|
||||||
|
entities=(
|
||||||
|
# No `not rep` stub carve-out on either side, unlike most exists_fn
|
||||||
|
# gates in this file -- entity creation only ever runs once, against
|
||||||
|
# whichever snapshot happens to be current the moment platforms are
|
||||||
|
# set up (see entity._is_included / __init__.py's
|
||||||
|
# async_config_entry_first_refresh-before-forward-entry-setups
|
||||||
|
# ordering), while flatten() re-evaluates exists_fn every poll
|
||||||
|
# against live data. Both descriptors share key='ai_energy_level',
|
||||||
|
# so if a stub carve-out let one of them win at setup time while the
|
||||||
|
# other wins once real data lands, flatten() would feed the
|
||||||
|
# instantiated entity a value shaped for the other platform (e.g. a
|
||||||
|
# bool into a Select). Requiring real, populated data on both sides
|
||||||
|
# keeps the entity-creation decision and the live-value decision in
|
||||||
|
# permanent agreement -- the cost is this entity doesn't appear
|
||||||
|
# until a reload if the device's very first poll stubs this
|
||||||
|
# cold-tier href, the same reload already required to fix which
|
||||||
|
# platform got picked in that case.
|
||||||
|
SwitchDesc(key='ai_energy_level', field='aiLevel',
|
||||||
|
name='AI energy level', icon='mdi:leaf',
|
||||||
|
entity_category='config',
|
||||||
|
value_fn=lambda v: v != '0',
|
||||||
|
exists_fn=lambda rep, resources: (
|
||||||
|
len(_ai_energy_supported_levels(rep)) == 1),
|
||||||
|
write_fn=_ai_energy_level_switch_write),
|
||||||
|
SelectDesc(key='ai_energy_level', field='aiLevel',
|
||||||
|
name='AI energy level', icon='mdi:leaf',
|
||||||
|
entity_category='config',
|
||||||
|
options=_ai_energy_level_options,
|
||||||
|
exists_fn=lambda rep, resources: (
|
||||||
|
len(_ai_energy_supported_levels(rep)) > 1),
|
||||||
|
write_fn=_ai_energy_level_write),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
FIRMWARE_UPDATE = Capability(
|
||||||
|
href='/otninformation/vs/0',
|
||||||
|
poll_tier='cold',
|
||||||
|
entities=(
|
||||||
|
BinarySensorDesc(
|
||||||
|
key='firmware_update',
|
||||||
|
field='x.com.samsung.da.newVersionAvailable',
|
||||||
|
name='Firmware update available',
|
||||||
|
device_class='update',
|
||||||
|
entity_category='diagnostic',
|
||||||
|
value_fn=lambda v: str(v).lower() == 'true' if v is not None else None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
SELF_CHECK = Capability(
|
||||||
|
href='/selfcheck/vs/0',
|
||||||
|
poll_tier='cold',
|
||||||
|
entities=(
|
||||||
|
SensorDesc(key='selfcheck_status', field='x.com.samsung.da.status',
|
||||||
|
name='Self-check status', icon='mdi:stethoscope',
|
||||||
|
entity_category='diagnostic'),
|
||||||
|
SensorDesc(key='selfcheck_result', field='x.com.samsung.da.result',
|
||||||
|
name='Self-check result', icon='mdi:clipboard-check-outline',
|
||||||
|
entity_category='diagnostic'),
|
||||||
|
# List of error codes from the last self-check; joined for display.
|
||||||
|
# Not every fridge reports the field, hence the exists_fn.
|
||||||
|
SensorDesc(key='selfcheck_error', field='x.com.samsung.da.error',
|
||||||
|
name='Self-check error', icon='mdi:alert-circle-outline',
|
||||||
|
entity_category='diagnostic',
|
||||||
|
exists_fn=lambda rep, resources: (
|
||||||
|
not rep or 'x.com.samsung.da.error' in rep),
|
||||||
|
value_fn=lambda v: (', '.join(v) if v else None) if isinstance(v, list) else v),
|
||||||
|
ButtonDesc(key='selfcheck_start', field='', name='Start self-check',
|
||||||
|
payload='Start', icon='mdi:play-circle-outline',
|
||||||
|
entity_category='diagnostic',
|
||||||
|
write_fn=lambda p, rep, href=None: (
|
||||||
|
['selfcheck', 'vs', '0'], {'x.com.samsung.da.status': p})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cross-family bundles, unpacked into every by_type registry's _build([...])
|
||||||
|
# call the same way ignored.IGNORED is (*common.UNIVERSAL / *common.POWER).
|
||||||
|
# discover() only binds a capability whose href is actually present in a
|
||||||
|
# given device's resource dump, so listing one here for a family that
|
||||||
|
# doesn't expose the href is a no-op, not a phantom entity -- see the
|
||||||
|
# adding-device-support skill's coverage-discipline section.
|
||||||
|
#
|
||||||
|
# UNIVERSAL holds every capability with no known family that both (a) has
|
||||||
|
# the href and (b) needs to model it some other way -- broadening one of
|
||||||
|
# these to a new family is a safe, harmless guess (issue #40's AI energy
|
||||||
|
# level: 2 of 6 families confirmed, blanket-added everywhere else).
|
||||||
|
#
|
||||||
|
# POWER is kept separate -- airconditioner is the one family that opts out
|
||||||
|
# of it. Canonical reason (see by_type/airconditioner.py and its test for
|
||||||
|
# pointers back here, not restatements): AC's climate entity already owns
|
||||||
|
# /power/0 and /power/vs/0 via bare, no-entity Capability objects
|
||||||
|
# (airconditioner.COVERAGE), and a second, real POWER_GENERIC/
|
||||||
|
# POWER_VS_FALLBACK cap on the same href would make _build() raise (a href
|
||||||
|
# with >1 cap must have every cap discriminated by rt_filter/match_fn, and
|
||||||
|
# the bare COVERAGE cap has neither). Kids-lock/remote-control don't have
|
||||||
|
# this conflict -- no AC dump has ever reported those hrefs -- so they stay
|
||||||
|
# in UNIVERSAL.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UNIVERSAL = (
|
||||||
|
ALARMS,
|
||||||
|
ENERGY_METER,
|
||||||
|
FIRMWARE_UPDATE,
|
||||||
|
SELF_CHECK,
|
||||||
|
AI_ENERGY_LEVEL,
|
||||||
|
KIDS_LOCK_GENERIC,
|
||||||
|
KIDS_LOCK_VS_FALLBACK,
|
||||||
|
REMOTE_CONTROL_GENERIC,
|
||||||
|
REMOTE_CONTROL_VS_FALLBACK,
|
||||||
|
)
|
||||||
|
|
||||||
|
POWER = (
|
||||||
|
POWER_GENERIC,
|
||||||
|
POWER_VS_FALLBACK,
|
||||||
|
)
|
||||||
@@ -8,7 +8,7 @@ wash, auto release dry) are read locally here.
|
|||||||
"""
|
"""
|
||||||
from ..capability import Capability
|
from ..capability import Capability
|
||||||
from ..entities import ButtonDesc, SelectDesc, SensorDesc, SwitchDesc
|
from ..entities import ButtonDesc, SelectDesc, SensorDesc, SwitchDesc
|
||||||
from .laundry import cycle_select, option_value, replace_in_options
|
from .laundry import bool_option_switch, cycle_select
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# /dishwasher/vs/0 — cycle wash/dry settings
|
# /dishwasher/vs/0 — cycle wash/dry settings
|
||||||
@@ -35,49 +35,20 @@ DISHWASHER_SETTINGS = Capability(
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# /course/vs/0 — cycle selection (shared laundry.cycle_select) plus the
|
# /course/vs/0 — cycle selection (shared laundry.cycle_select) plus the
|
||||||
# dishwasher-only StormWashZone / AutoDoorRelease toggles that ride in the
|
# dishwasher-only StormWashZone / AutoDoorRelease toggles that ride in the
|
||||||
# same options array. Course display names live in translations under
|
# same options array (shared laundry.bool_option_switch, same options[]
|
||||||
|
# boolean-toggle contract washer's bubble-soak/pre-wash/intensive switches
|
||||||
|
# use). Course display names live in translations under
|
||||||
# entity.select.dishwasher_cycle (see laundry.cycle_select).
|
# entity.select.dishwasher_cycle (see laundry.cycle_select).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _storm_wash_write(p, rep, href=None):
|
|
||||||
if p not in ('On', 'Off'):
|
|
||||||
return None
|
|
||||||
opts = list(rep.get('x.com.samsung.da.options') or [])
|
|
||||||
if not opts:
|
|
||||||
return None
|
|
||||||
return ['course', 'vs', '0'], {
|
|
||||||
'x.com.samsung.da.options': replace_in_options(opts, 'StormWashZone', p),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _auto_release_write(p, rep, href=None):
|
|
||||||
if p not in ('On', 'Off'):
|
|
||||||
return None
|
|
||||||
opts = list(rep.get('x.com.samsung.da.options') or [])
|
|
||||||
if not opts:
|
|
||||||
return None
|
|
||||||
return ['course', 'vs', '0'], {
|
|
||||||
'x.com.samsung.da.options': replace_in_options(opts, 'AutoDoorRelease', p),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
CYCLE_OPTIONS = Capability(
|
CYCLE_OPTIONS = Capability(
|
||||||
href='/course/vs/0',
|
href='/course/vs/0',
|
||||||
entities=(
|
entities=(
|
||||||
cycle_select(translation_key='dishwasher_cycle', icon='mdi:dishwasher'),
|
cycle_select(translation_key='dishwasher_cycle', icon='mdi:dishwasher'),
|
||||||
SwitchDesc(key='storm_wash', name='Storm Wash+', icon='mdi:weather-lightning-rainy',
|
bool_option_switch('storm_wash', 'Storm Wash+', 'mdi:weather-lightning-rainy',
|
||||||
rep_fn=lambda rep: option_value(
|
'StormWashZone'),
|
||||||
rep.get('x.com.samsung.da.options'), 'StormWashZone') == 'On',
|
bool_option_switch('auto_release_dry', 'Auto release dry', 'mdi:door-open',
|
||||||
write_fn=_storm_wash_write),
|
'AutoDoorRelease', gate_on_presence=True),
|
||||||
SwitchDesc(key='auto_release_dry', name='Auto release dry', icon='mdi:door-open',
|
|
||||||
exists_fn=lambda rep, resources: any(
|
|
||||||
isinstance(o, str) and o.startswith('AutoDoorRelease_')
|
|
||||||
for o in (rep.get('x.com.samsung.da.options') or [])
|
|
||||||
),
|
|
||||||
rep_fn=lambda rep: option_value(
|
|
||||||
rep.get('x.com.samsung.da.options'), 'AutoDoorRelease') == 'On',
|
|
||||||
write_fn=_auto_release_write),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import datetime
|
|||||||
|
|
||||||
from ..capability import Capability
|
from ..capability import Capability
|
||||||
from ..entities import (
|
from ..entities import (
|
||||||
BinarySensorDesc, ButtonDesc, NumberDesc, SelectDesc, SensorDesc,
|
BinarySensorDesc, NumberDesc, SelectDesc, SensorDesc,
|
||||||
SwitchDesc, TimeDesc,
|
SwitchDesc, TimeDesc,
|
||||||
)
|
)
|
||||||
from .common import normalize_temp_unit
|
from .common import normalize_temp_unit
|
||||||
@@ -100,6 +100,12 @@ ICEMAKER_NIGHTTIME = Capability(
|
|||||||
# Icemaker (generic — covers /icemaker/one/vs/0, /icemaker/two/vs/0)
|
# Icemaker (generic — covers /icemaker/one/vs/0, /icemaker/two/vs/0)
|
||||||
# /icemaker/status/vs/0 is kept as exact-href cap and binds first.
|
# /icemaker/status/vs/0 is kept as exact-href cap and binds first.
|
||||||
# /icemaker/nighttime/vs/0 is excluded by match_fn (lacks iceMaker.state).
|
# /icemaker/nighttime/vs/0 is excluded by match_fn (lacks iceMaker.state).
|
||||||
|
#
|
||||||
|
# Entity names are derived from x.com.samsung.da.iceMaker.name ("CUBED_ICE",
|
||||||
|
# "ICE_BITES") via name_field, not the href's "one"/"two" segment -- these two
|
||||||
|
# ice makers are independent on/off toggles that can both be enabled at once
|
||||||
|
# (issue #27), so they stay separate entities rather than a single ice-type
|
||||||
|
# select, but users still want them labeled with the device's own names.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _icemaker_write(field):
|
def _icemaker_write(field):
|
||||||
@@ -112,6 +118,7 @@ ICEMAKER_GENERIC = Capability(
|
|||||||
href=None,
|
href=None,
|
||||||
href_prefix='/icemaker/',
|
href_prefix='/icemaker/',
|
||||||
match_fn=lambda rep, resources: 'x.com.samsung.da.iceMaker.state' in rep,
|
match_fn=lambda rep, resources: 'x.com.samsung.da.iceMaker.state' in rep,
|
||||||
|
name_field='x.com.samsung.da.iceMaker.name',
|
||||||
poll_tier='warm',
|
poll_tier='warm',
|
||||||
entities=(
|
entities=(
|
||||||
SensorDesc(key='making_status',
|
SensorDesc(key='making_status',
|
||||||
@@ -209,6 +216,14 @@ DEFROST_DELAY = Capability(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# OCF-native boolean mirror of DEFROST_DELAY. The captured TP1X_REF_21K
|
||||||
|
# firmware publishes the same state on both hrefs, but only the vendor resource
|
||||||
|
# above has a confirmed write contract. Bind the native mirror without another
|
||||||
|
# entity so discovery records it as an intentional duplicate.
|
||||||
|
DEFROST_DELAY_NATIVE_DUPLICATE = Capability(
|
||||||
|
href='/defrost/delay/0',
|
||||||
|
)
|
||||||
|
|
||||||
DEFROST_BLOCK_STATUS = Capability(
|
DEFROST_BLOCK_STATUS = Capability(
|
||||||
href='/defrost/block/vs/0',
|
href='/defrost/block/vs/0',
|
||||||
poll_tier='warm',
|
poll_tier='warm',
|
||||||
@@ -220,28 +235,6 @@ DEFROST_BLOCK_STATUS = Capability(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Self-check diagnostic
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
SELF_CHECK = Capability(
|
|
||||||
href='/selfcheck/vs/0',
|
|
||||||
poll_tier='cold',
|
|
||||||
entities=(
|
|
||||||
SensorDesc(key='selfcheck_status', field='x.com.samsung.da.status',
|
|
||||||
name='Self-check status', icon='mdi:stethoscope',
|
|
||||||
entity_category='diagnostic'),
|
|
||||||
SensorDesc(key='selfcheck_result', field='x.com.samsung.da.result',
|
|
||||||
name='Self-check result', icon='mdi:clipboard-check-outline',
|
|
||||||
entity_category='diagnostic'),
|
|
||||||
ButtonDesc(key='selfcheck_start', field='', name='Start self-check',
|
|
||||||
payload='Start', icon='mdi:play-circle-outline',
|
|
||||||
entity_category='diagnostic',
|
|
||||||
write_fn=lambda p, rep, href=None: (
|
|
||||||
['selfcheck', 'vs', '0'], {'x.com.samsung.da.status': p})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Refrigeration modes (rapid cooling)
|
# Refrigeration modes (rapid cooling)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -494,17 +487,64 @@ BEVERAGE_ZONE = Capability(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Pantry / Cool Select Zone -- a convertible compartment toggled between
|
||||||
|
# wine/deli/drinks temperature presets (issue #20). Same shape as
|
||||||
|
# BEVERAGE_ZONE (a controllable named sub-zone with a mode + supported-modes
|
||||||
|
# list) but a distinct resource/field set -- x.com.samsung.da.mode /
|
||||||
|
# x.com.samsung.da.supportedOptions on /status/pantry/one/vs/0, rather than
|
||||||
|
# roomDesiredMode/roomSupportedModes on /specialzone/one/vs/0. Only a "one"
|
||||||
|
# instance has been seen; not generalized to a pattern cap until a second
|
||||||
|
# instance turns up.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _pantry_write(p, rep, href=None):
|
||||||
|
return ['status', 'pantry', 'one', 'vs', '0'], {'x.com.samsung.da.mode': p}
|
||||||
|
|
||||||
|
|
||||||
|
PANTRY_ZONE = Capability(
|
||||||
|
href='/status/pantry/one/vs/0',
|
||||||
|
poll_tier='warm',
|
||||||
|
entities=(
|
||||||
|
SelectDesc(key='pantry_zone_mode', field='x.com.samsung.da.mode',
|
||||||
|
name='Pantry zone mode', icon='mdi:glass-wine',
|
||||||
|
translation_key='pantry_zone_mode',
|
||||||
|
entity_category='config',
|
||||||
|
options_field='x.com.samsung.da.supportedOptions',
|
||||||
|
write_fn=_pantry_write),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Flex zone (convertible drawer — /mode/vs/0 on RF9000-class fridges)
|
# Flex zone (convertible drawer — /mode/vs/0 on RF9000-class fridges)
|
||||||
#
|
#
|
||||||
# x.com.samsung.da.modes holds multiple orthogonal flags in one list.
|
# x.com.samsung.da.modes holds multiple orthogonal flags in one list; the
|
||||||
# The flex zone entry is identified by the CV_TTYPE_RF9000A_ prefix.
|
# flex-zone entry is whichever item is also a member of supportedOptions --
|
||||||
|
# the other flags (WATERFILTER_*, DEFROST_BLOCK_*, the CVN_*_ZONE marker)
|
||||||
|
# never appear there. The prefix on that item varies by fridge family
|
||||||
|
# (CV_TTYPE_RF9000A_ on RF9000-class, CV_FDR_ on Bespoke-class -- issue #27 /
|
||||||
|
# #26, where the old CV_TTYPE_RF9000A_-only match left this entity bound but
|
||||||
|
# stuck on None), so match by list membership instead of a hardcoded prefix.
|
||||||
# Write replaces only that item; other flags are preserved.
|
# Write replaces only that item; other flags are preserved.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _flex_zone_supported(rep):
|
||||||
|
return set(rep.get('x.com.samsung.da.supportedOptions') or ())
|
||||||
|
|
||||||
|
|
||||||
|
def _flex_zone_current(rep):
|
||||||
|
# Every dump seen has at most one modes/supportedOptions overlap, so
|
||||||
|
# "first match" and "strip all matches" (in the write below) agree. If a
|
||||||
|
# future device ever reports two, this reads the first and the write
|
||||||
|
# would drop both -- revisit if that turns up.
|
||||||
|
modes = rep.get('x.com.samsung.da.modes') or []
|
||||||
|
supported = _flex_zone_supported(rep)
|
||||||
|
return next((m for m in modes if m in supported), None)
|
||||||
|
|
||||||
|
|
||||||
def _flex_zone_write(p, rep, href=None):
|
def _flex_zone_write(p, rep, href=None):
|
||||||
modes = list(rep.get('x.com.samsung.da.modes') or [])
|
supported = _flex_zone_supported(rep)
|
||||||
modes = [m for m in modes if not m.startswith('CV_TTYPE_RF9000A_')]
|
modes = [m for m in (rep.get('x.com.samsung.da.modes') or []) if m not in supported]
|
||||||
modes.append(p)
|
modes.append(p)
|
||||||
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': modes}
|
return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': modes}
|
||||||
|
|
||||||
@@ -514,39 +554,17 @@ FLEX_ZONE = Capability(
|
|||||||
poll_tier='warm',
|
poll_tier='warm',
|
||||||
entities=(
|
entities=(
|
||||||
SelectDesc(key='flex_zone_mode',
|
SelectDesc(key='flex_zone_mode',
|
||||||
field='x.com.samsung.da.modes',
|
|
||||||
name='Flex zone mode', icon='mdi:thermostat',
|
name='Flex zone mode', icon='mdi:thermostat',
|
||||||
translation_key='flex_zone_mode',
|
translation_key='flex_zone_mode',
|
||||||
entity_category='config',
|
entity_category='config',
|
||||||
options_field='x.com.samsung.da.supportedOptions',
|
options_field='x.com.samsung.da.supportedOptions',
|
||||||
exists_fn=lambda rep, resources: bool(
|
exists_fn=lambda rep, resources: bool(
|
||||||
rep.get('x.com.samsung.da.supportedOptions')),
|
rep.get('x.com.samsung.da.supportedOptions')),
|
||||||
value_fn=lambda modes: next(
|
rep_fn=_flex_zone_current,
|
||||||
(m for m in (modes or [])
|
|
||||||
if m.startswith('CV_TTYPE_RF9000A_')), None),
|
|
||||||
write_fn=_flex_zone_write),
|
write_fn=_flex_zone_write),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Firmware update
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
FIRMWARE_UPDATE = Capability(
|
|
||||||
href='/otninformation/vs/0',
|
|
||||||
poll_tier='cold',
|
|
||||||
entities=(
|
|
||||||
BinarySensorDesc(
|
|
||||||
key='firmware_update',
|
|
||||||
field='x.com.samsung.da.newVersionAvailable',
|
|
||||||
name='Firmware update available',
|
|
||||||
device_class='update',
|
|
||||||
entity_category='diagnostic',
|
|
||||||
value_fn=lambda v: str(v).lower() == 'true' if v is not None else None,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Generic door pattern capability (href=None — use as pattern_cap only)
|
# Generic door pattern capability (href=None — use as pattern_cap only)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -655,6 +673,14 @@ ICEMAKER_STATUS_FALLBACK = Capability(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# OCF-native aggregate mirror of ICEMAKER_STATUS_FALLBACK. On the captured
|
||||||
|
# TP1X_REF_21K it duplicates both the vendor aggregate and the richer per-unit
|
||||||
|
# /icemaker/one|two/vs/0 resources. Its write contract is not advertised, so
|
||||||
|
# keep the proven per-unit/vendor controls and bind this as a duplicate only.
|
||||||
|
ICEMAKER_STATUS_NATIVE_DUPLICATE = Capability(
|
||||||
|
href='/icemaker/status/0',
|
||||||
|
)
|
||||||
|
|
||||||
# OCF-native /refrigeration/0 (issue #7's unbound_hrefs) -- the odd one out
|
# OCF-native /refrigeration/0 (issue #7's unbound_hrefs) -- the odd one out
|
||||||
# in this section: its three fields duplicate two *different* richer
|
# in this section: its three fields duplicate two *different* richer
|
||||||
# hrefs (REFRIGERATION's rapidFridge/rapidFreezing and
|
# hrefs (REFRIGERATION's rapidFridge/rapidFreezing and
|
||||||
|
|||||||
@@ -85,14 +85,9 @@ IGNORED: list[Capability] = [
|
|||||||
# Warranty/service-plan enrollment status — every field reads "Unknown"
|
# Warranty/service-plan enrollment status — every field reads "Unknown"
|
||||||
# on hardware not enrolled in a Samsung Care+ style program.
|
# on hardware not enrolled in a Samsung Care+ style program.
|
||||||
Capability(href='/dginformation/vs/0'),
|
Capability(href='/dginformation/vs/0'),
|
||||||
# AI energy-saving level; only one value ('1') is ever in
|
|
||||||
# supportedAiLevel on hardware seen so far, so there's no real choice
|
|
||||||
# to expose. Revisit if a device surfaces more than one supported level.
|
|
||||||
Capability(href='/energy/ailevel/vs/0'),
|
|
||||||
# OCF-native vacation-mode flag (fridge). Only one value ('RVACATION_OFF')
|
# OCF-native vacation-mode flag (fridge). Only one value ('RVACATION_OFF')
|
||||||
# has ever been seen in `modes` (issue #7's dump), same "no real choice
|
# has ever been seen in `modes` (issue #7's dump) -- no real choice to
|
||||||
# to expose yet" situation as /energy/ailevel/vs/0 above. Revisit if a
|
# expose yet. Revisit if a device surfaces it toggled on.
|
||||||
# device surfaces it toggled on.
|
|
||||||
Capability(href='/mode/0'),
|
Capability(href='/mode/0'),
|
||||||
# Opaque integer with no supportedModes/options list to interpret it
|
# Opaque integer with no supportedModes/options list to interpret it
|
||||||
# against — meaning unclear from the raw resource alone.
|
# against — meaning unclear from the raw resource alone.
|
||||||
|
|||||||
@@ -217,6 +217,59 @@ def cycle_select(*, translation_key, icon):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Plain boolean toggles over /course/vs/0's options[] array: a
|
||||||
|
# '<prefix>_On'/'<prefix>_Off' token, read-modify-written the same way as
|
||||||
|
# the 'Course' token above. Shared by washer (bubble soak, pre-wash,
|
||||||
|
# intensive -- issue #22) and dishwasher (storm wash, auto release dry) --
|
||||||
|
# both families ride this exact contract, just with different prefixes and
|
||||||
|
# different presence/validation needs on top.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def bool_option_write(prefix):
|
||||||
|
def write(p, rep, href=None):
|
||||||
|
if p not in ('On', 'Off'):
|
||||||
|
return None
|
||||||
|
opts = list(rep.get('x.com.samsung.da.options') or [])
|
||||||
|
if not opts:
|
||||||
|
return None
|
||||||
|
return ['course', 'vs', '0'], {
|
||||||
|
'x.com.samsung.da.options': replace_in_options(opts, prefix, p),
|
||||||
|
}
|
||||||
|
return write
|
||||||
|
|
||||||
|
|
||||||
|
def bool_option_value(prefix):
|
||||||
|
return lambda rep: option_value(rep.get('x.com.samsung.da.options'), prefix) == 'On'
|
||||||
|
|
||||||
|
|
||||||
|
def bool_option_exists(prefix):
|
||||||
|
return lambda rep, resources: option_value(
|
||||||
|
rep.get('x.com.samsung.da.options'), prefix) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def bool_option_switch(key, name, icon, prefix, *, entity_category=None,
|
||||||
|
gate_on_presence=False, validate_fn=None):
|
||||||
|
"""A SwitchDesc over a '<prefix>_On'/'<prefix>_Off' options[] token.
|
||||||
|
|
||||||
|
gate_on_presence self-gates the entity off on models that never report
|
||||||
|
the token at all (washer's bubble soak/pre-wash/intensive); leave False
|
||||||
|
for a toggle every device in the family reports (dishwasher's storm
|
||||||
|
wash). validate_fn is passed straight through to SwitchDesc for callers
|
||||||
|
that need to reject a write against live device state (e.g. washer's
|
||||||
|
per-course availability check) -- this factory has no opinion on it and
|
||||||
|
building one, if needed, is the caller's job.
|
||||||
|
"""
|
||||||
|
return SwitchDesc(
|
||||||
|
key=key, name=name, icon=icon, entity_category=entity_category,
|
||||||
|
exists_fn=bool_option_exists(prefix) if gate_on_presence else None,
|
||||||
|
rep_fn=bool_option_value(prefix),
|
||||||
|
write_fn=bool_option_write(prefix),
|
||||||
|
validate_fn=validate_fn,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# /wm/jobbeginingstatus/vs/0 -- the "why did the cycle not start" reason
|
# /wm/jobbeginingstatus/vs/0 -- the "why did the cycle not start" reason
|
||||||
# (e.g. door open, no water). The vendor field is x.com.samsung.da.currentStatus
|
# (e.g. door open, no water). The vendor field is x.com.samsung.da.currentStatus
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ SETPOINT_MIN_C = 30
|
|||||||
SETPOINT_MAX_C = 270
|
SETPOINT_MAX_C = 270
|
||||||
SETPOINT_STEP_C = 5
|
SETPOINT_STEP_C = 5
|
||||||
|
|
||||||
|
# Verified against issue #44's range dump (NSI6DG9100SRAA, unit reported as
|
||||||
|
# "Fahrenheit" on /temperatures/vs/0): Bake mode's modeSpec on /mode/vs/0
|
||||||
|
# reports tempMinF/tempMaxF/tempIntervalF = 175/550/5. Kept as a separate
|
||||||
|
# constant set rather than converted from the Celsius bounds above, which
|
||||||
|
# are themselves unverified (no live dump; see module docstring).
|
||||||
|
SETPOINT_MIN_F = 175
|
||||||
|
SETPOINT_MAX_F = 550
|
||||||
|
SETPOINT_STEP_F = 5
|
||||||
|
|
||||||
# Mode options seen on NV7000BS-class. No dump exists so this list is inferred
|
# Mode options seen on NV7000BS-class. No dump exists so this list is inferred
|
||||||
# from Samsung documentation and firmware observations. The firmware will
|
# from Samsung documentation and firmware observations. The firmware will
|
||||||
# reject unknown modes; missing entries here are a coverage gap, not a bug.
|
# reject unknown modes; missing entries here are a coverage gap, not a bug.
|
||||||
@@ -129,8 +138,9 @@ def _oven_setpoint_write(p, rep, href=None):
|
|||||||
temp = float(p)
|
temp = float(p)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
temp_i = int(round(temp / SETPOINT_STEP_C) * SETPOINT_STEP_C)
|
min_v, max_v, step_v = _setpoint_bounds(rep)
|
||||||
if not (SETPOINT_MIN_C <= temp_i <= SETPOINT_MAX_C):
|
temp_i = int(round(temp / step_v) * step_v)
|
||||||
|
if not (min_v <= temp_i <= max_v):
|
||||||
return None
|
return None
|
||||||
items = rep.get('x.com.samsung.da.items')
|
items = rep.get('x.com.samsung.da.items')
|
||||||
if not items:
|
if not items:
|
||||||
@@ -263,14 +273,24 @@ def _oven_temp_unit(rep):
|
|||||||
same aggregate `/temperatures/vs/0` items[] resource type, which on
|
same aggregate `/temperatures/vs/0` items[] resource type, which on
|
||||||
fridge hardware carries a per-item `x.com.samsung.da.unit` field
|
fridge hardware carries a per-item `x.com.samsung.da.unit` field
|
||||||
('Celsius'/'Fahrenheit') that was previously hardcoded away (issue #7).
|
('Celsius'/'Fahrenheit') that was previously hardcoded away (issue #7).
|
||||||
No live oven dump has surfaced a non-Celsius reading yet, so this keeps
|
Keeps the verified '°C' default when the field is absent (the original
|
||||||
the verified '°C' default when the field is absent, but reads it live
|
NV7000BS-class dump this module was written against), but reads it live
|
||||||
if a device ever reports otherwise."""
|
-- issue #44's range dump is the first to report 'Fahrenheit' here."""
|
||||||
items = rep.get('x.com.samsung.da.items') or []
|
items = rep.get('x.com.samsung.da.items') or []
|
||||||
unit = items[0].get('x.com.samsung.da.unit') if items else None
|
unit = items[0].get('x.com.samsung.da.unit') if items else None
|
||||||
return normalize_temp_unit(unit, default='°C')
|
return normalize_temp_unit(unit, default='°C')
|
||||||
|
|
||||||
|
|
||||||
|
def _setpoint_bounds(rep):
|
||||||
|
"""(min, max, step) for the live unit -- see the SETPOINT_*_C/_F
|
||||||
|
constants above for provenance. Bounds must track the unit shown by
|
||||||
|
unit_fn (both read the same live rep), or the HA slider's range would
|
||||||
|
silently mismatch its own displayed unit."""
|
||||||
|
if _oven_temp_unit(rep) == '°F':
|
||||||
|
return SETPOINT_MIN_F, SETPOINT_MAX_F, SETPOINT_STEP_F
|
||||||
|
return SETPOINT_MIN_C, SETPOINT_MAX_C, SETPOINT_STEP_C
|
||||||
|
|
||||||
|
|
||||||
OVEN_SETPOINT = Capability(
|
OVEN_SETPOINT = Capability(
|
||||||
href='/temperatures/vs/0',
|
href='/temperatures/vs/0',
|
||||||
poll_tier='hot',
|
poll_tier='hot',
|
||||||
@@ -280,6 +300,9 @@ OVEN_SETPOINT = Capability(
|
|||||||
name='Setpoint', device_class='temperature', unit_fn=_oven_temp_unit,
|
name='Setpoint', device_class='temperature', unit_fn=_oven_temp_unit,
|
||||||
native_min=float(SETPOINT_MIN_C), native_max=float(SETPOINT_MAX_C),
|
native_min=float(SETPOINT_MIN_C), native_max=float(SETPOINT_MAX_C),
|
||||||
step=float(SETPOINT_STEP_C), icon='mdi:thermometer-chevron-up',
|
step=float(SETPOINT_STEP_C), icon='mdi:thermometer-chevron-up',
|
||||||
|
native_min_fn=lambda rep: float(_setpoint_bounds(rep)[0]),
|
||||||
|
native_max_fn=lambda rep: float(_setpoint_bounds(rep)[1]),
|
||||||
|
step_fn=lambda rep: float(_setpoint_bounds(rep)[2]),
|
||||||
value_fn=lambda items: _int(
|
value_fn=lambda items: _int(
|
||||||
(items[0].get('x.com.samsung.da.desired') if items else None)),
|
(items[0].get('x.com.samsung.da.desired') if items else None)),
|
||||||
write_fn=_oven_setpoint_write),
|
write_fn=_oven_setpoint_write),
|
||||||
@@ -314,6 +337,13 @@ OVEN_CONNECTED = Capability(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Static cavity capability metadata (count/type/supported features) -- no
|
||||||
|
# per-cavity data varies at runtime on any dump seen so far (issue #44's
|
||||||
|
# range: single cavity, no supportedFeatureList entries). Bound with no
|
||||||
|
# entities purely for coverage; revisit if a multi-cavity dump surfaces
|
||||||
|
# fields worth exposing.
|
||||||
|
OVEN_SPEC = Capability(href='/oven/spec/vs/0')
|
||||||
|
|
||||||
OVEN_MODE = Capability(
|
OVEN_MODE = Capability(
|
||||||
href='/mode/vs/0',
|
href='/mode/vs/0',
|
||||||
poll_tier='warm',
|
poll_tier='warm',
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
"""Capabilities for the cooktop half of range/combo appliances (issue #44,
|
||||||
|
model TP1X_DA-KS-RANGE-0102X).
|
||||||
|
|
||||||
|
Not to be confused with PR #23's registry/capabilities/cooktop.py, which
|
||||||
|
covers an unrelated standalone-cooktop product (NA9300K-class) that encodes
|
||||||
|
burner state as strings inside /mode/vs/0's options array instead of the
|
||||||
|
structured /cooktop/status/vs/0 resource this module reads -- two different
|
||||||
|
OCF surfaces that happen to share the English word "cooktop".
|
||||||
|
|
||||||
|
Unlike the rest of the OCF surface, these hrefs use plain camelCase field
|
||||||
|
names (no `x.com.samsung.da.` prefix) -- `/cooktop/status/vs/0` already
|
||||||
|
looks like a vendor resource migrated onto OCF-standard-shaped field naming.
|
||||||
|
|
||||||
|
`/cooktop/status/vs/0` carries every burner's live state in one `burnerList`
|
||||||
|
array (indexed by `burnerNumber`, not by a separate href per burner like
|
||||||
|
fridge ice makers), so per-burner entities are hardcoded up to MAX_BURNERS
|
||||||
|
and gated by exists_fn against whichever indices the device actually
|
||||||
|
reports -- harmless over-declaration, per common.py's UNIVERSAL note, since
|
||||||
|
an index absent from burnerList just never binds.
|
||||||
|
|
||||||
|
Write surfaces here are unproven (no live device to verify against, same
|
||||||
|
caveat as oven.py's RMW writes) -- power level uses the same read-modify-
|
||||||
|
write pattern already proven safe elsewhere in this codebase (oven setpoint,
|
||||||
|
icemaker toggles).
|
||||||
|
"""
|
||||||
|
from ..capability import Capability
|
||||||
|
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc
|
||||||
|
|
||||||
|
# Observed as high as 4 (this issue's dump); user-reported hardware with 5
|
||||||
|
# burners exists. Kept a little above both since exists_fn gates unused
|
||||||
|
# slots out -- see module docstring.
|
||||||
|
MAX_BURNERS = 6
|
||||||
|
|
||||||
|
|
||||||
|
def _burner(burner_list, i):
|
||||||
|
for b in (burner_list or []):
|
||||||
|
if b.get('burnerNumber') == i:
|
||||||
|
return b
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _burner_exists(i):
|
||||||
|
return lambda rep, resources: _burner(rep.get('burnerList'), i) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _burner_field_fn(i, field):
|
||||||
|
return lambda burner_list: (_burner(burner_list, i) or {}).get(field)
|
||||||
|
|
||||||
|
|
||||||
|
def _burner_hot_surface_fn(i):
|
||||||
|
get_state = _burner_field_fn(i, 'hotSurfaceState')
|
||||||
|
return lambda burner_list: get_state(burner_list) not in (None, 'normal')
|
||||||
|
|
||||||
|
|
||||||
|
def _power_level_options(resources):
|
||||||
|
spec = resources.get('/cooktop/spec/vs/0') or {}
|
||||||
|
return list(spec.get('supportedPowerLevelList') or [])
|
||||||
|
|
||||||
|
|
||||||
|
def _burner_power_level_write(i):
|
||||||
|
def write(p, rep, href=None):
|
||||||
|
burner_list = rep.get('burnerList')
|
||||||
|
if not burner_list:
|
||||||
|
return None
|
||||||
|
new_list = []
|
||||||
|
found = False
|
||||||
|
for b in burner_list:
|
||||||
|
if b.get('burnerNumber') == i:
|
||||||
|
b = dict(b)
|
||||||
|
b['powerLevel'] = p
|
||||||
|
found = True
|
||||||
|
new_list.append(b)
|
||||||
|
if not found:
|
||||||
|
return None
|
||||||
|
return ['cooktop', 'status', 'vs', '0'], {'burnerList': new_list}
|
||||||
|
return write
|
||||||
|
|
||||||
|
|
||||||
|
def _burner_entities(i):
|
||||||
|
exists = _burner_exists(i)
|
||||||
|
n = i + 1
|
||||||
|
return (
|
||||||
|
SelectDesc(key=f'burner_{i}_power_level', field='burnerList',
|
||||||
|
name=f'Burner {n} power level', icon='mdi:knob',
|
||||||
|
options=_power_level_options,
|
||||||
|
exists_fn=exists,
|
||||||
|
value_fn=_burner_field_fn(i, 'powerLevel'),
|
||||||
|
write_fn=_burner_power_level_write(i)),
|
||||||
|
SensorDesc(key=f'burner_{i}_state', field='burnerList',
|
||||||
|
name=f'Burner {n} state', icon='mdi:stove',
|
||||||
|
exists_fn=exists,
|
||||||
|
value_fn=_burner_field_fn(i, 'operationState')),
|
||||||
|
BinarySensorDesc(key=f'burner_{i}_hot_surface', field='burnerList',
|
||||||
|
name=f'Burner {n} hot surface', device_class='heat',
|
||||||
|
exists_fn=exists,
|
||||||
|
value_fn=_burner_hot_surface_fn(i)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
COOKTOP_STATUS = Capability(
|
||||||
|
href='/cooktop/status/vs/0',
|
||||||
|
poll_tier='hot',
|
||||||
|
entities=(
|
||||||
|
SensorDesc(key='cooktop_state', field='operationState',
|
||||||
|
name='Cooktop state', icon='mdi:pot-steam'),
|
||||||
|
*[e for i in range(MAX_BURNERS) for e in _burner_entities(i)],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Static burner-count/power-level-list metadata, read directly by
|
||||||
|
# COOKTOP_STATUS's power-level select (options=_power_level_options) rather
|
||||||
|
# than exposed through its own entity -- same "informs another capability,
|
||||||
|
# no entity of its own" pattern as /wm/editcourse/vs/0 (ignored.py).
|
||||||
|
COOKTOP_SPEC = Capability(href='/cooktop/spec/vs/0')
|
||||||
|
|
||||||
|
# settingTime (seconds) is the hot-surface auto-shutoff timer's configured
|
||||||
|
# duration (1200s = 20 min in issue #44's dump); state on/off is whether the
|
||||||
|
# feature itself is enabled -- not a live "surface is hot right now" alert
|
||||||
|
# (that's COOKTOP_STATUS's per-burner hot_surface). No write contract
|
||||||
|
# verified, so read-only for now.
|
||||||
|
COOKTOP_SAFETY = Capability(
|
||||||
|
href='/cooktop/settings/status/vs/0',
|
||||||
|
poll_tier='warm',
|
||||||
|
entities=(
|
||||||
|
BinarySensorDesc(key='cooktop_safety_shutoff_enabled', field='safetyAlert',
|
||||||
|
name='Hot surface auto-shutoff enabled',
|
||||||
|
entity_category='config',
|
||||||
|
value_fn=lambda v: (v or {}).get('state') == 'on'),
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -17,10 +17,13 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
from ..capability import Capability
|
from ..capability import Capability
|
||||||
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc
|
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc
|
||||||
from .laundry import cycle_select, hex_pairs, option_value, replace_in_options
|
from .laundry import (
|
||||||
|
bool_option_exists, bool_option_switch, cycle_options, cycle_select, hex_pairs, option_value,
|
||||||
|
replace_in_options,
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Course_XX hex codes. The 23 codes named in strings.json/translations
|
# Course_XX hex codes. 23 of the codes named in strings.json/translations
|
||||||
# under entity.select.washer_cycle.state.<id, lowercased> were captured
|
# under entity.select.washer_cycle.state.<id, lowercased> were captured
|
||||||
# from a live WW90DG6U25LEU4's x.com.samsung.da.editCourseList
|
# from a live WW90DG6U25LEU4's x.com.samsung.da.editCourseList
|
||||||
# (EditCourseList_1C1D211B1E29243328262722202325322F2E30662D8F96), matched
|
# (EditCourseList_1C1D211B1E29243328262722202325322F2E30662D8F96), matched
|
||||||
@@ -35,6 +38,13 @@ from .laundry import cycle_select, hex_pairs, option_value, replace_in_options
|
|||||||
# you'd expect to be pinned (default cycle + maintenance cycle),
|
# you'd expect to be pinned (default cycle + maintenance cycle),
|
||||||
# corroborating the positional match.
|
# corroborating the positional match.
|
||||||
#
|
#
|
||||||
|
# A further 5 codes -- '36' Wash+Dry, '37' Air Wash, '38' Cotton Dry,
|
||||||
|
# '39' Synthetics Dry, and a second, distinct '1F' Intense Cold (not the
|
||||||
|
# same code as '8F' above) -- came from a WD90T654DBN/S1 washer/dryer
|
||||||
|
# combo's editCourseList and were named from that user's app screenshot
|
||||||
|
# (issue #22). Combo units carry their own course set, so these codes
|
||||||
|
# don't imply anything about '1F' on a plain washer.
|
||||||
|
#
|
||||||
# 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
|
||||||
@@ -82,6 +92,19 @@ WASHER_SETTINGS = Capability(
|
|||||||
options_field='x.com.samsung.da.supportedRinseCycles',
|
options_field='x.com.samsung.da.supportedRinseCycles',
|
||||||
write_fn=lambda p, rep, href=None: (
|
write_fn=lambda p, rep, href=None: (
|
||||||
['washer', 'vs', '0'], {'x.com.samsung.da.rinseCycles': p})),
|
['washer', 'vs', '0'], {'x.com.samsung.da.rinseCycles': p})),
|
||||||
|
# Washer/dryer combo units carry a dryLevel field on the wash
|
||||||
|
# resource itself (no separate dryer device/course) -- see issue
|
||||||
|
# #22. Self-gates off on plain washers, which never report
|
||||||
|
# supportedDryLevel.
|
||||||
|
SelectDesc(key='dry_level', field='x.com.samsung.da.dryLevel',
|
||||||
|
name='Dry level', icon='mdi:tumble-dryer',
|
||||||
|
entity_category='config',
|
||||||
|
translation_key='washer_dry_level',
|
||||||
|
options_field='x.com.samsung.da.supportedDryLevel',
|
||||||
|
exists_fn=lambda rep, resources: bool(
|
||||||
|
rep.get('x.com.samsung.da.supportedDryLevel')),
|
||||||
|
write_fn=lambda p, rep, href=None: (
|
||||||
|
['washer', 'vs', '0'], {'x.com.samsung.da.dryLevel': p})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -213,9 +236,58 @@ def _dosing_low(prefix):
|
|||||||
rep.get('x.com.samsung.da.options'), prefix) not in (None, 'Off')
|
rep.get('x.com.samsung.da.options'), prefix) not in (None, 'Off')
|
||||||
|
|
||||||
|
|
||||||
def _dosing_alarm_exists(prefix):
|
# Bubble soak / pre-wash / intensive-wash toggles, from the same options[]
|
||||||
return lambda rep, resources: option_value(
|
# array (issue #22 follow-up on a WD90T654DBN/S1 combo). Each rides as a
|
||||||
rep.get('x.com.samsung.da.options'), prefix) is not None
|
# plain '<Prefix>_On'/'<Prefix>_Off' token, confirmed by a dump taken with
|
||||||
|
# Bubble Soak switched on in the app (BubbleSoak_On) -- the same On/Off shape
|
||||||
|
# already used by AiOption and KidsLockBypass in this same array, so
|
||||||
|
# PreWashSetting/IntensiveSetting are assumed to follow suit.
|
||||||
|
#
|
||||||
|
# Each also has a differently-named hex-pair availability field that lines up
|
||||||
|
# positionally with editCourseList: BubbleSoakSet, PreWashAvailableSet,
|
||||||
|
# IntensiveAvailableSet. On the reporter's dump (course '30' at position 1 of
|
||||||
|
# 24), all three read 'F0' at that position and the toggle was writable --
|
||||||
|
# and the same dump's earlier state (course '1C' at position 0, 'BubbleSoak
|
||||||
|
# Off') decodes to '00' for that course, matching the app graying the
|
||||||
|
# control out there. 'F0'/'00' is treated as available/unavailable on that
|
||||||
|
# evidence. exists_fn (device-level presence) still only runs once, against
|
||||||
|
# the setup-time snapshot, so it isn't a fit for this per-course check --
|
||||||
|
# validate_fn runs on every write attempt instead (dispatched from
|
||||||
|
# coordinator.async_send_command, ahead of write_fn), rejecting an on-write
|
||||||
|
# for a course whose byte isn't 'F0' with a user-facing error rather than
|
||||||
|
# silently no-opping against the device. The read/write/presence machinery
|
||||||
|
# itself is laundry.bool_option_switch, shared with dishwasher's storm-wash/
|
||||||
|
# auto-release-dry toggles -- only this per-course gating is washer-only, so
|
||||||
|
# it stays here rather than in laundry.py (see laundry.bool_option_switch's
|
||||||
|
# docstring: it takes a prebuilt validate_fn and has no opinion on it).
|
||||||
|
def _bool_option_switch(key, name, icon, prefix, availability_field):
|
||||||
|
def validate(p, rep, resources):
|
||||||
|
"""Reject turning on when the selected course's byte in
|
||||||
|
`availability_field` isn't 'F0'. Turning off is never blocked. Falls
|
||||||
|
back to allowing the write whenever the availability data can't be
|
||||||
|
resolved (unrecognized course, missing/mismatched-length bitmap)
|
||||||
|
rather than guessing -- a false rejection is worse than an
|
||||||
|
occasional no-op write."""
|
||||||
|
if p != 'On':
|
||||||
|
return None
|
||||||
|
opts = rep.get('x.com.samsung.da.options') or []
|
||||||
|
current = option_value(opts, 'Course')
|
||||||
|
courses = cycle_options(resources)
|
||||||
|
if not current or current not in courses:
|
||||||
|
return None
|
||||||
|
raw = option_value(opts, availability_field)
|
||||||
|
if raw is None:
|
||||||
|
return None
|
||||||
|
pairs = hex_pairs(raw)
|
||||||
|
if len(pairs) != len(courses):
|
||||||
|
return None
|
||||||
|
if pairs[courses.index(current)] != 'F0':
|
||||||
|
return f"{name} isn't available on the selected cycle."
|
||||||
|
return None
|
||||||
|
|
||||||
|
return bool_option_switch(
|
||||||
|
key, name, icon, prefix,
|
||||||
|
entity_category='config', gate_on_presence=True, validate_fn=validate)
|
||||||
|
|
||||||
|
|
||||||
WASHER_COURSE = Capability(
|
WASHER_COURSE = Capability(
|
||||||
@@ -268,11 +340,17 @@ WASHER_COURSE = Capability(
|
|||||||
write_fn=_level_write('SoftenerLevel2Ctrl')),
|
write_fn=_level_write('SoftenerLevel2Ctrl')),
|
||||||
BinarySensorDesc(key='detergent_low', name='Detergent low',
|
BinarySensorDesc(key='detergent_low', name='Detergent low',
|
||||||
icon='mdi:alert-circle-outline', device_class='problem',
|
icon='mdi:alert-circle-outline', device_class='problem',
|
||||||
exists_fn=_dosing_alarm_exists('DetergentAlarm'),
|
exists_fn=bool_option_exists('DetergentAlarm'),
|
||||||
rep_fn=_dosing_low('DetergentAlarm')),
|
rep_fn=_dosing_low('DetergentAlarm')),
|
||||||
BinarySensorDesc(key='softener_low', name='Softener low',
|
BinarySensorDesc(key='softener_low', name='Softener low',
|
||||||
icon='mdi:alert-circle-outline', device_class='problem',
|
icon='mdi:alert-circle-outline', device_class='problem',
|
||||||
exists_fn=_dosing_alarm_exists('SoftenerAlarm'),
|
exists_fn=bool_option_exists('SoftenerAlarm'),
|
||||||
rep_fn=_dosing_low('SoftenerAlarm')),
|
rep_fn=_dosing_low('SoftenerAlarm')),
|
||||||
|
_bool_option_switch('bubble_soak', 'Bubble soak', 'mdi:chart-bubble',
|
||||||
|
'BubbleSoak', 'BubbleSoakSet'),
|
||||||
|
_bool_option_switch('pre_wash', 'Pre wash', 'mdi:washing-machine',
|
||||||
|
'PreWashSetting', 'PreWashAvailableSet'),
|
||||||
|
_bool_option_switch('intensive', 'Intensive', 'mdi:washing-machine',
|
||||||
|
'IntensiveSetting', 'IntensiveAvailableSet'),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -15,6 +15,11 @@ class Capability:
|
|||||||
rt_filter: Optional[str] = None # bind only if rt_filter in rep.get('rt', ())
|
rt_filter: Optional[str] = None # bind only if rt_filter in rep.get('rt', ())
|
||||||
href_prefix: Optional[str] = None # pattern caps only: bind only if href starts with this
|
href_prefix: Optional[str] = None # pattern caps only: bind only if href starts with this
|
||||||
strip_prefix_in_key: bool = False # strip href_prefix segs before building key_override
|
strip_prefix_in_key: bool = False # strip href_prefix segs before building key_override
|
||||||
|
# Rep field holding this instance's device-given name (e.g. an ice
|
||||||
|
# maker's "CUBED_ICE"/"ICE_BITES"), normalized and used as the display
|
||||||
|
# name prefix in place of the href-derived instance label. Does not
|
||||||
|
# affect key_override/unique_id -- only what's shown in the UI.
|
||||||
|
name_field: Optional[str] = None
|
||||||
match_fn: Optional[Callable[[dict, dict], bool]] = None # match_fn(rep, resources) -> bool
|
match_fn: Optional[Callable[[dict, dict], bool]] = None # match_fn(rep, resources) -> bool
|
||||||
# Rare optional hook — only operational-state-style resources use this.
|
# Rare optional hook — only operational-state-style resources use this.
|
||||||
on_observation: Optional[Callable[[dict, dict], None]] = None
|
on_observation: Optional[Callable[[dict, dict], None]] = None
|
||||||
|
|||||||
@@ -27,6 +27,25 @@ class BoundEntity:
|
|||||||
desc: SamsungEntityDescription
|
desc: SamsungEntityDescription
|
||||||
instance: str = ''
|
instance: str = ''
|
||||||
key_override: Optional[str] = None
|
key_override: Optional[str] = None
|
||||||
|
instance_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _snake_to_title(s: str) -> str:
|
||||||
|
"""'CUBED_ICE'/'cubed_ice' -> 'Cubed Ice'. Shared with entity.py's
|
||||||
|
_derive_name, which applies the same transform to a state key."""
|
||||||
|
return s.replace('_', ' ').title()
|
||||||
|
|
||||||
|
|
||||||
|
def _instance_name(cap: Capability, rep: dict) -> Optional[str]:
|
||||||
|
"""Normalize `cap.name_field`'s raw value ("CUBED_ICE" -> "Cubed Ice")
|
||||||
|
for use as a display-name prefix, or None if the cap doesn't declare
|
||||||
|
one or the device didn't report it."""
|
||||||
|
if not cap.name_field:
|
||||||
|
return None
|
||||||
|
raw = rep.get(cap.name_field)
|
||||||
|
if not isinstance(raw, str) or not raw:
|
||||||
|
return None
|
||||||
|
return _snake_to_title(raw)
|
||||||
|
|
||||||
|
|
||||||
def instance_suffix(href: str) -> str:
|
def instance_suffix(href: str) -> str:
|
||||||
@@ -37,6 +56,18 @@ def instance_suffix(href: str) -> str:
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def _bind(cap: Capability, href: str, inst: str, inst_name: Optional[str],
|
||||||
|
key_prefix: Optional[str] = None) -> list[BoundEntity]:
|
||||||
|
"""Build one BoundEntity per entity on `cap`, sharing the instance/
|
||||||
|
key-prefix/instance-name computed once by the caller."""
|
||||||
|
return [
|
||||||
|
BoundEntity(href=href, capability=cap, desc=desc, instance=inst,
|
||||||
|
key_override=f'{key_prefix}_{desc.key}' if key_prefix else None,
|
||||||
|
instance_name=inst_name)
|
||||||
|
for desc in cap.entities
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def discover(
|
def discover(
|
||||||
resources: dict[str, dict],
|
resources: dict[str, dict],
|
||||||
registry: dict[str, list[Capability]],
|
registry: dict[str, list[Capability]],
|
||||||
@@ -58,9 +89,7 @@ def discover(
|
|||||||
if cap.match_fn is not None and not cap.match_fn(rep, resources):
|
if cap.match_fn is not None and not cap.match_fn(rep, resources):
|
||||||
continue
|
continue
|
||||||
inst = instance_suffix(href)
|
inst = instance_suffix(href)
|
||||||
for desc in cap.entities:
|
out.extend(_bind(cap, href, inst, _instance_name(cap, rep)))
|
||||||
out.append(BoundEntity(href=href, capability=cap,
|
|
||||||
desc=desc, instance=inst))
|
|
||||||
matched = True
|
matched = True
|
||||||
|
|
||||||
if matched:
|
if matched:
|
||||||
@@ -78,10 +107,7 @@ def discover(
|
|||||||
# Auto-derive key prefix from href segments (skip digits and 'vs')
|
# Auto-derive key prefix from href segments (skip digits and 'vs')
|
||||||
src = href[len(cap.href_prefix):] if (cap.strip_prefix_in_key and cap.href_prefix) else href
|
src = href[len(cap.href_prefix):] if (cap.strip_prefix_in_key and cap.href_prefix) else href
|
||||||
segs = [s for s in src.strip('/').split('/') if s and not s.isdigit() and s != 'vs']
|
segs = [s for s in src.strip('/').split('/') if s and not s.isdigit() and s != 'vs']
|
||||||
for desc in cap.entities:
|
out.extend(_bind(cap, href, inst, _instance_name(cap, rep), '_'.join(segs)))
|
||||||
key_override = '_'.join(segs) + '_' + desc.key
|
|
||||||
out.append(BoundEntity(href=href, capability=cap, desc=desc,
|
|
||||||
instance=inst, key_override=key_override))
|
|
||||||
matched = True
|
matched = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
Frozen dataclasses so the future native HA component can consume them as
|
Frozen dataclasses so the future native HA component can consume them as
|
||||||
EntityDescription subclasses unchanged. Read transforms live in value_fn;
|
EntityDescription subclasses unchanged. Read transforms live in value_fn;
|
||||||
presence gating in exists_fn; write logic in write_fn on command platforms.
|
presence gating in exists_fn; write logic in write_fn on command platforms;
|
||||||
|
pre-write rejection (surfaced to the user, not just logged) in validate_fn
|
||||||
|
where a description declares one.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -10,6 +12,11 @@ from dataclasses import dataclass, field
|
|||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
WriteFn = Optional[Callable[[Any, dict], "tuple[list[str], dict] | None"]]
|
WriteFn = Optional[Callable[[Any, dict], "tuple[list[str], dict] | None"]]
|
||||||
|
# (payload, rep, resources) -> a human-readable rejection message, or None to
|
||||||
|
# allow the write. resources is the coordinator's full href->rep snapshot, for
|
||||||
|
# the same cross-resource lookups exists_fn needs (e.g. reading a sibling
|
||||||
|
# href's live option list).
|
||||||
|
ValidateFn = Optional[Callable[[Any, dict, dict], "str | None"]]
|
||||||
|
|
||||||
|
|
||||||
def _identity(v: Any) -> Any:
|
def _identity(v: Any) -> Any:
|
||||||
@@ -61,6 +68,7 @@ class SelectDesc(SamsungEntityDescription):
|
|||||||
class SwitchDesc(SamsungEntityDescription):
|
class SwitchDesc(SamsungEntityDescription):
|
||||||
device_class: Optional[str] = None
|
device_class: Optional[str] = None
|
||||||
write_fn: WriteFn = None
|
write_fn: WriteFn = None
|
||||||
|
validate_fn: ValidateFn = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
@@ -77,6 +85,13 @@ class NumberDesc(SamsungEntityDescription):
|
|||||||
native_min: Optional[float] = None
|
native_min: Optional[float] = None
|
||||||
native_max: Optional[float] = None
|
native_max: Optional[float] = None
|
||||||
step: Optional[float] = None
|
step: Optional[float] = None
|
||||||
|
# Override native_min/native_max/step from the live rep, when set --
|
||||||
|
# same "static default, live override" shape as unit_fn, for resources
|
||||||
|
# whose sane bounds depend on a per-device value (e.g. a temperature
|
||||||
|
# setpoint reported in Celsius on one device, Fahrenheit on another).
|
||||||
|
native_min_fn: Optional[Callable[[dict], float]] = None
|
||||||
|
native_max_fn: Optional[Callable[[dict], float]] = None
|
||||||
|
step_fn: Optional[Callable[[dict], float]] = None
|
||||||
range_field: Optional[str] = None # resource field containing [min, max] list
|
range_field: Optional[str] = None # resource field containing [min, max] list
|
||||||
write_fn: WriteFn = None
|
write_fn: WriteFn = None
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,25 @@
|
|||||||
"sp_ttype_wine_dessert": "Wine and Dessert"
|
"sp_ttype_wine_dessert": "Wine and Dessert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pantry_zone_mode": {
|
||||||
|
"state": {
|
||||||
|
"fdr_wine": "Wine",
|
||||||
|
"fdr_deli": "Deli",
|
||||||
|
"fdr_drinks": "Drinks"
|
||||||
|
}
|
||||||
|
},
|
||||||
"flex_zone_mode": {
|
"flex_zone_mode": {
|
||||||
"state": {
|
"state": {
|
||||||
"cv_ttype_rf9000a_freeze": "Freeze",
|
"cv_ttype_rf9000a_freeze": "Freeze",
|
||||||
"cv_ttype_rf9000a_softfreeze": "Soft Freeze",
|
"cv_ttype_rf9000a_softfreeze": "Soft Freeze",
|
||||||
"cv_ttype_rf9000a_meat_fish": "Meat/Fish",
|
"cv_ttype_rf9000a_meat_fish": "Meat/Fish",
|
||||||
"cv_ttype_rf9000a_fruit_veggies": "Fruit & Veggies",
|
"cv_ttype_rf9000a_fruit_veggies": "Fruit & Veggies",
|
||||||
"cv_ttype_rf9000a_beverage": "Beverage"
|
"cv_ttype_rf9000a_beverage": "Beverage",
|
||||||
|
"cv_fdr_wine": "Wine",
|
||||||
|
"cv_fdr_deli": "Deli",
|
||||||
|
"cv_fdr_beverage": "Beverage",
|
||||||
|
"cv_fdr_meat": "Meat",
|
||||||
|
"cv_fdr_soft_freezer": "Soft Freezer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dishwasher_cycle": {
|
"dishwasher_cycle": {
|
||||||
@@ -77,7 +89,12 @@
|
|||||||
"66": "Denim",
|
"66": "Denim",
|
||||||
"2d": "Silent Wash",
|
"2d": "Silent Wash",
|
||||||
"8f": "Intense Cold",
|
"8f": "Intense Cold",
|
||||||
"96": "Less Microfiber"
|
"96": "Less Microfiber",
|
||||||
|
"36": "Wash+Dry",
|
||||||
|
"37": "Air Wash",
|
||||||
|
"38": "Cotton Dry",
|
||||||
|
"39": "Synthetics Dry",
|
||||||
|
"1f": "Intense Cold"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dryer_cycle": {
|
"dryer_cycle": {
|
||||||
@@ -119,6 +136,18 @@
|
|||||||
"02": "2x",
|
"02": "2x",
|
||||||
"03": "3x"
|
"03": "3x"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"washer_dry_level": {
|
||||||
|
"state": {
|
||||||
|
"none": "Off",
|
||||||
|
"cupboard": "Cupboard",
|
||||||
|
"30": "30 min",
|
||||||
|
"60": "1 hr",
|
||||||
|
"90": "1 hr 30",
|
||||||
|
"120": "2 hr",
|
||||||
|
"180": "3 hr",
|
||||||
|
"240": "4 hr"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
|||||||
@@ -30,13 +30,25 @@
|
|||||||
"sp_ttype_wine_dessert": "Wine and Dessert"
|
"sp_ttype_wine_dessert": "Wine and Dessert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pantry_zone_mode": {
|
||||||
|
"state": {
|
||||||
|
"fdr_wine": "Wine",
|
||||||
|
"fdr_deli": "Deli",
|
||||||
|
"fdr_drinks": "Drinks"
|
||||||
|
}
|
||||||
|
},
|
||||||
"flex_zone_mode": {
|
"flex_zone_mode": {
|
||||||
"state": {
|
"state": {
|
||||||
"cv_ttype_rf9000a_freeze": "Freeze",
|
"cv_ttype_rf9000a_freeze": "Freeze",
|
||||||
"cv_ttype_rf9000a_softfreeze": "Soft Freeze",
|
"cv_ttype_rf9000a_softfreeze": "Soft Freeze",
|
||||||
"cv_ttype_rf9000a_meat_fish": "Meat/Fish",
|
"cv_ttype_rf9000a_meat_fish": "Meat/Fish",
|
||||||
"cv_ttype_rf9000a_fruit_veggies": "Fruit & Veggies",
|
"cv_ttype_rf9000a_fruit_veggies": "Fruit & Veggies",
|
||||||
"cv_ttype_rf9000a_beverage": "Beverage"
|
"cv_ttype_rf9000a_beverage": "Beverage",
|
||||||
|
"cv_fdr_wine": "Wine",
|
||||||
|
"cv_fdr_deli": "Deli",
|
||||||
|
"cv_fdr_beverage": "Beverage",
|
||||||
|
"cv_fdr_meat": "Meat",
|
||||||
|
"cv_fdr_soft_freezer": "Soft Freezer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dishwasher_cycle": {
|
"dishwasher_cycle": {
|
||||||
@@ -77,7 +89,12 @@
|
|||||||
"66": "Denim",
|
"66": "Denim",
|
||||||
"2d": "Silent Wash",
|
"2d": "Silent Wash",
|
||||||
"8f": "Intense Cold",
|
"8f": "Intense Cold",
|
||||||
"96": "Less Microfiber"
|
"96": "Less Microfiber",
|
||||||
|
"36": "Wash+Dry",
|
||||||
|
"37": "Air Wash",
|
||||||
|
"38": "Cotton Dry",
|
||||||
|
"39": "Synthetics Dry",
|
||||||
|
"1f": "Intense Cold"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dryer_cycle": {
|
"dryer_cycle": {
|
||||||
@@ -119,6 +136,18 @@
|
|||||||
"02": "2x",
|
"02": "2x",
|
||||||
"03": "3x"
|
"03": "3x"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"washer_dry_level": {
|
||||||
|
"state": {
|
||||||
|
"none": "Off",
|
||||||
|
"cupboard": "Cupboard",
|
||||||
|
"30": "30 min",
|
||||||
|
"60": "1 hr",
|
||||||
|
"90": "1 hr 30",
|
||||||
|
"120": "2 hr",
|
||||||
|
"180": "3 hr",
|
||||||
|
"240": "4 hr"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
|||||||
@@ -0,0 +1,595 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"note": "Samsung Room A/C, model TP1X_DA-AC-RAC-01011 (oneUiVersion '7.0 Air conditioner', Tizen Lite). Scrubbed real /device/0 dump. Adds temperature via /temperatures/vs/0 + display light. Issue #17 (newer model class).",
|
||||||
|
"issue": 17,
|
||||||
|
"model": "TP1X_DA-AC-RAC-01011",
|
||||||
|
"oneUiVersion": "7.0 Air conditioner"
|
||||||
|
},
|
||||||
|
"device0": [
|
||||||
|
{
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.devcol",
|
||||||
|
"oic.wk.col"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.ll",
|
||||||
|
"oic.if.b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/airlevelcheck/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.periodicSensingInterval": "1800",
|
||||||
|
"x.com.samsung.da.sensingState": "NonProcessing",
|
||||||
|
"x.com.samsung.da.lastSensingTime": "1784508715",
|
||||||
|
"x.com.samsung.da.lastSensingLevel": "Kr1",
|
||||||
|
"x.com.samsung.da.periodicSensingSkipStatus": "Off",
|
||||||
|
"x.com.samsung.da.periodicSensingSkipTime": "00000000",
|
||||||
|
"x.com.samsung.da.periodicSensingActivationState": "On",
|
||||||
|
"x.com.samsung.da.autoExeState": "Alarm",
|
||||||
|
"x.com.samsung.da.supportedAutoExeState": [
|
||||||
|
"Airpurify",
|
||||||
|
"Alarm",
|
||||||
|
"Sensing"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.startSensingOnce": "Off",
|
||||||
|
"x.com.samsung.da.autoExeSetting": "Off",
|
||||||
|
"x.com.samsung.da.supportedAutoExeSetting": [
|
||||||
|
"On",
|
||||||
|
"Off"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/aisleep/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.displayNightMode": "Off",
|
||||||
|
"x.com.samsung.da.elapsedTime": "0",
|
||||||
|
"x.com.samsung.da.requestFeedback": "Off",
|
||||||
|
"x.com.samsung.da.resultFeedback": 0,
|
||||||
|
"x.com.samsung.da.statusFeedback": "Idle",
|
||||||
|
"x.com.samsung.da.sleepTime": "14002200"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/alarms/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.alarms"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.a"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.alarmType": "Device",
|
||||||
|
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||||
|
"x.com.samsung.da.triggeredTime": "2026-07-22T07:13:19",
|
||||||
|
"x.com.samsung.da.state": "Deleted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.alarmType": "Device",
|
||||||
|
"x.com.samsung.da.code": "FilterAlarm_OFF",
|
||||||
|
"x.com.samsung.da.triggeredTime": "2026-07-22T07:13:19",
|
||||||
|
"x.com.samsung.da.state": "Deleted"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/availablecontrolsets/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.sets": "000000A0012C016102490F0F0000",
|
||||||
|
"x.com.samsung.da.id": "RAC",
|
||||||
|
"x.com.samsung.da.version": "1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/configuration/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.region": "0000000000",
|
||||||
|
"x.com.samsung.da.airconOptionList": [
|
||||||
|
"PRODUCT_GLOBAL",
|
||||||
|
"AI_GLOBAL_HEATPUMP_4.0",
|
||||||
|
"AI_3.0",
|
||||||
|
"Auto_To_AI",
|
||||||
|
"AI_Heat",
|
||||||
|
"ENERGY_2.0",
|
||||||
|
"HOMECARE_WIZARD_V2",
|
||||||
|
"DR",
|
||||||
|
"SingleCommand_1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/da/softreset/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.softwarereset": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/drlc/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.start": "1970-01-01T00:00:00Z",
|
||||||
|
"x.com.samsung.da.override": "Off",
|
||||||
|
"x.com.samsung.da.durationminutes": "0",
|
||||||
|
"x.com.samsung.da.drlcLevel": "0",
|
||||||
|
"x.com.samsung.da.realSaving": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/energy/consumption/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.cumulativeDate": "1784704371",
|
||||||
|
"x.com.samsung.da.cumulativeSavedPower": "-1",
|
||||||
|
"x.com.samsung.da.cumulativePower": "75025",
|
||||||
|
"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": "+02:00",
|
||||||
|
"x.com.samsung.supprtedtype": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/filter/airdustfilter/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.filterUsageResolution": "1",
|
||||||
|
"x.com.samsung.da.filterCapacity": "500",
|
||||||
|
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||||
|
"x.com.samsung.da.filterResetType": [
|
||||||
|
"replaceable",
|
||||||
|
"washable"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.filterStatus": "normal",
|
||||||
|
"x.com.samsung.da.filterUsage": "42",
|
||||||
|
"x.com.samsung.da.filterDesiredUsage": "500",
|
||||||
|
"x.com.samsung.da.supportedFilterDesiredUsage": [
|
||||||
|
"180",
|
||||||
|
"300",
|
||||||
|
"500",
|
||||||
|
"700"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/humidity/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.humidity": "0",
|
||||||
|
"x.com.samsung.da.fivepercentHumidity": "53"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.modelNum": "TP1X_DA-AC-RAC-01011_0000|10275643|60010523001911F58E0049320092E900",
|
||||||
|
"x.com.samsung.da.description": "TP1X_DA-AC-RAC-01011_0000",
|
||||||
|
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.serialNumOption": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Software",
|
||||||
|
"x.com.samsung.da.number": "02762A260401",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0",
|
||||||
|
"x.com.samsung.da.number": "02756C25082500,FFFFFFFFFFFFFF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "2",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0",
|
||||||
|
"x.com.samsung.da.number": "02669A24092600,02636A10001200"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "3",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0",
|
||||||
|
"x.com.samsung.da.number": "02672A10001000,FFFFFFFFFFFFFF"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.diagDumpType": "file",
|
||||||
|
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||||
|
"x.com.samsung.da.diagLogType": [
|
||||||
|
"errCode",
|
||||||
|
"dump"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.diagMnid": "0AJT",
|
||||||
|
"x.com.samsung.da.diagSetupid": "AR4",
|
||||||
|
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||||
|
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||||
|
"x.com.samsung.da.diagTsId": "DA01"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/keepnormalstate/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.keepnormal": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/light/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"mode": "On",
|
||||||
|
"supportedModes": [
|
||||||
|
"On",
|
||||||
|
"Off"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mds/absencemonitoring/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mds/absencestate/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/convenient/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modes": "Off",
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"Off",
|
||||||
|
"Sleep",
|
||||||
|
"Quiet",
|
||||||
|
"Smart",
|
||||||
|
"Speed",
|
||||||
|
"MotionIndirect",
|
||||||
|
"Nano",
|
||||||
|
"NanoSleep",
|
||||||
|
"DryComfort"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.mode"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.a"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modes": [
|
||||||
|
"Heat"
|
||||||
|
],
|
||||||
|
"workingMode": "Heat",
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"Auto",
|
||||||
|
"Cool",
|
||||||
|
"Dry",
|
||||||
|
"Heat",
|
||||||
|
"Wind"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.options": [
|
||||||
|
"OptionCode_56378",
|
||||||
|
"Sleep_6",
|
||||||
|
"ArtificialWorking_Off",
|
||||||
|
"Off",
|
||||||
|
"OutdoorTemp_68",
|
||||||
|
"OutdoorConnection_Connected"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/option/airpurify/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.option.airpurify"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.a"
|
||||||
|
],
|
||||||
|
"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.supportedStatus": [
|
||||||
|
"Start",
|
||||||
|
"Stop"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.progress": "0",
|
||||||
|
"x.com.samsung.da.settingStatus": "On",
|
||||||
|
"x.com.samsung.da.supportedSettingStatus": [
|
||||||
|
"On",
|
||||||
|
"Off"
|
||||||
|
],
|
||||||
|
"defaultSettingStatus": "On"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/option/muteonce/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"muteonce": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/personality/presence/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/power/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.power": "Off",
|
||||||
|
"operationNumber": "39",
|
||||||
|
"displayCondition": "Enable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/realtimenotiforclient/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.timeforshortnoti": "0",
|
||||||
|
"x.com.samsung.da.longnotisubscription": "true",
|
||||||
|
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/remotedatacontrol/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.status": "Off",
|
||||||
|
"x.com.samsung.da.connectionStatus": "Disconnected"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/remotetemperature/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.temperature": "",
|
||||||
|
"x.com.samsung.da.unit": "",
|
||||||
|
"x.com.samsung.da.error": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/reserverulesets/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.sets": "3EFFFFFFFF101E121EFFFF101EFFFF000001001F0001001F001F0000009CACFFFF0000",
|
||||||
|
"x.com.samsung.da.id": "RAC",
|
||||||
|
"x.com.samsung.da.version": "1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/selfcheck/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.start": "Cancel",
|
||||||
|
"x.com.samsung.da.status": "Ready",
|
||||||
|
"x.com.samsung.da.progress": "0",
|
||||||
|
"x.com.samsung.da.result": "NA",
|
||||||
|
"x.com.samsung.da.supportedActions": [
|
||||||
|
"Start",
|
||||||
|
"Cancel"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.error": [
|
||||||
|
"DA_ERROR_NONE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/sensors/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.cleanLevel": "1",
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Sensor for CleanLevel",
|
||||||
|
"x.com.samsung.da.type": "CleanLevel",
|
||||||
|
"x.com.samsung.da.value": [
|
||||||
|
"1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "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": "/temperatures/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Temperature",
|
||||||
|
"x.com.samsung.da.current": "17.0",
|
||||||
|
"x.com.samsung.da.desired": "25.0",
|
||||||
|
"x.com.samsung.da.minimum": "16",
|
||||||
|
"x.com.samsung.da.maximum": "30",
|
||||||
|
"x.com.samsung.da.increment": "1",
|
||||||
|
"x.com.samsung.da.unit": "Celsius"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/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",
|
||||||
|
"Up_And_Low",
|
||||||
|
"Left_And_Right",
|
||||||
|
"All"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wind/strength/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modes": "0",
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modesName": [
|
||||||
|
"Auto",
|
||||||
|
"Low",
|
||||||
|
"Mid",
|
||||||
|
"High",
|
||||||
|
"Turbo"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/otninformation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.target": "",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "false",
|
||||||
|
"otnStatus": "None",
|
||||||
|
"flashingProgress": "0",
|
||||||
|
"otnCompleteDate": "noHistory",
|
||||||
|
"scheduledTime": "None",
|
||||||
|
"swVersionInfo": {
|
||||||
|
"platform": "Tizen Lite",
|
||||||
|
"oneUiVersion": "7.0 Air conditioner",
|
||||||
|
"osVersion": "4.0"
|
||||||
|
},
|
||||||
|
"otnList": [
|
||||||
|
{
|
||||||
|
"type": "WIFI",
|
||||||
|
"modelId": "ARA-KR-TP1-25-ARXX00",
|
||||||
|
"versions": [
|
||||||
|
"11260401"
|
||||||
|
],
|
||||||
|
"visVersion": "260401"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/timezone/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"timezoneid": "Africa/Johannesburg",
|
||||||
|
"offset": "+02:00",
|
||||||
|
"DST": "OFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/connectionconfig/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"autoReconnectionMinVersion": "1.0",
|
||||||
|
"autoReconnection": "true",
|
||||||
|
"autoReconnectionProtocolType": [
|
||||||
|
"helper_hotspot",
|
||||||
|
"ble_ocf"
|
||||||
|
],
|
||||||
|
"supportedWiFiAuthType": [
|
||||||
|
"OPEN",
|
||||||
|
"WEP",
|
||||||
|
"WPA-PSK",
|
||||||
|
"WPA2-PSK",
|
||||||
|
"SAE"
|
||||||
|
],
|
||||||
|
"supportedWiFiCryptoType": [
|
||||||
|
"TKIP",
|
||||||
|
"AES",
|
||||||
|
"WEP-64",
|
||||||
|
"WEP-128"
|
||||||
|
],
|
||||||
|
"supportedWiFiFreq": [
|
||||||
|
"2.4G"
|
||||||
|
],
|
||||||
|
"calmConnectionCare": {
|
||||||
|
"version": "1.0",
|
||||||
|
"role": [
|
||||||
|
"things"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wirelessinfo/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"macaddressWiFi": "**REDACTED**",
|
||||||
|
"macaddressBLE": "**REDACTED**",
|
||||||
|
"connectedApSsid": "**REDACTED**"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/quickcontrol/info/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"supportedVersion": "1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/dginformation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"enrolmentstatus": "Unknown",
|
||||||
|
"devicestate": "Unknown",
|
||||||
|
"lockstatus": "Normal",
|
||||||
|
"nextduedate": "",
|
||||||
|
"workingminutes": 0,
|
||||||
|
"paymentinfo": {
|
||||||
|
"emiplan": "Unknown",
|
||||||
|
"currency": "Unknown",
|
||||||
|
"totalemi": 0,
|
||||||
|
"totalemipaid": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+591
@@ -0,0 +1,591 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"model": "TP1X_DA-AC-RAC-01001_0000",
|
||||||
|
"device_type": "airconditioner",
|
||||||
|
"source": "issue #38 diagnostics (scrubbed)",
|
||||||
|
"note": "Newer TP1X RAC board (oneUiVersion '7.0 Air conditioner') with a fuller resource set than the ARTIK051/TP2X boards: display light, self-check, mute-once, and a circuit-breaker current-limit setting."
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.alarms"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.a"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.alarmType": "Device",
|
||||||
|
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||||
|
"x.com.samsung.da.triggeredTime": "2026-07-22T12:58:00",
|
||||||
|
"x.com.samsung.da.state": "Deleted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.alarmType": "Device",
|
||||||
|
"x.com.samsung.da.code": "FilterAlarm_OFF",
|
||||||
|
"x.com.samsung.da.triggeredTime": "2026-07-22T12:58:00",
|
||||||
|
"x.com.samsung.da.state": "Deleted"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/availablecontrolsets/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.sets": "AEB700A0012C01610248040F0000",
|
||||||
|
"x.com.samsung.da.id": "RAC",
|
||||||
|
"x.com.samsung.da.version": "1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/configuration/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.region": "0000000000",
|
||||||
|
"x.com.samsung.da.airconOptionList": [
|
||||||
|
"PRODUCT_GLOBAL",
|
||||||
|
"AI_GLOBAL_HEATPUMP_4.0",
|
||||||
|
"AI_3.0",
|
||||||
|
"Auto_To_AI",
|
||||||
|
"AI_Heat",
|
||||||
|
"ENERGY_2.0",
|
||||||
|
"HOMECARE_WIZARD_V2",
|
||||||
|
"DR",
|
||||||
|
"SingleCommand_1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "/da/softreset/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.softwarereset": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/dginformation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"enrolmentstatus": "Unknown",
|
||||||
|
"devicestate": "Unknown",
|
||||||
|
"lockstatus": "Unknown",
|
||||||
|
"nextduedate": "",
|
||||||
|
"workingminutes": 0,
|
||||||
|
"paymentinfo": {
|
||||||
|
"emiplan": "Unknown",
|
||||||
|
"currency": "Unknown",
|
||||||
|
"totalemi": 0,
|
||||||
|
"totalemipaid": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/drlc/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.start": "2026-07-22T10:59:14Z",
|
||||||
|
"x.com.samsung.da.override": "Off",
|
||||||
|
"x.com.samsung.da.durationminutes": "1148",
|
||||||
|
"x.com.samsung.da.drlcLevel": "1",
|
||||||
|
"x.com.samsung.da.realSaving": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/electriccurrent/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"operation": "Off",
|
||||||
|
"modes": "3",
|
||||||
|
"supportedModes": [
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
"9"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/energy/consumption/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.cumulativeDate": "1784725078",
|
||||||
|
"x.com.samsung.da.cumulativeSavedPower": "-1",
|
||||||
|
"x.com.samsung.da.cumulativePower": "160922",
|
||||||
|
"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": "+03:00",
|
||||||
|
"x.com.samsung.supprtedtype": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/filter/airdustfilter/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.filterUsageResolution": "1",
|
||||||
|
"x.com.samsung.da.filterCapacity": "500",
|
||||||
|
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||||
|
"x.com.samsung.da.filterResetType": [
|
||||||
|
"replaceable",
|
||||||
|
"washable"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.filterStatus": "normal",
|
||||||
|
"x.com.samsung.da.filterUsage": "30",
|
||||||
|
"x.com.samsung.da.filterDesiredUsage": "500",
|
||||||
|
"x.com.samsung.da.supportedFilterDesiredUsage": [
|
||||||
|
"180",
|
||||||
|
"300",
|
||||||
|
"500",
|
||||||
|
"700"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.modelNum": "TP1X_DA-AC-RAC-01001_0000|10267841|60010532001911000E0041220090A000",
|
||||||
|
"x.com.samsung.da.description": "TP1X_DA-AC-RAC-01001_0000",
|
||||||
|
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.serialNumOption": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Software",
|
||||||
|
"x.com.samsung.da.number": "02762A260401",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0",
|
||||||
|
"x.com.samsung.da.number": "02678A24092500,FFFFFFFFFFFFFF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "2",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0",
|
||||||
|
"x.com.samsung.da.number": "02669A24092600,02636A10001200"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "3",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0",
|
||||||
|
"x.com.samsung.da.number": "02672A10001000,FFFFFFFFFFFFFF"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.diagDumpType": "file",
|
||||||
|
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||||
|
"x.com.samsung.da.diagLogType": [
|
||||||
|
"errCode",
|
||||||
|
"dump"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.diagMnid": "0AJT",
|
||||||
|
"x.com.samsung.da.diagSetupid": "AR2",
|
||||||
|
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||||
|
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||||
|
"x.com.samsung.da.diagTsId": "DA01"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/keepnormalstate/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.keepnormal": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/light/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"mode": "On",
|
||||||
|
"supportedModes": [
|
||||||
|
"On",
|
||||||
|
"Off"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mds/absencemonitoring/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mds/absencestate/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/convenient/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modes": "Off",
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"Off",
|
||||||
|
"Sleep",
|
||||||
|
"Quiet",
|
||||||
|
"Smart",
|
||||||
|
"Speed"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.mode"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.a"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modes": [
|
||||||
|
"Cool"
|
||||||
|
],
|
||||||
|
"workingMode": "Cool",
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"Auto",
|
||||||
|
"Cool",
|
||||||
|
"Dry",
|
||||||
|
"Fan",
|
||||||
|
"Heat"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.options": [
|
||||||
|
"OptionCode_54328",
|
||||||
|
"Sleep_0",
|
||||||
|
"ArtificialWorking_Off",
|
||||||
|
"ComfortAICooling_Off",
|
||||||
|
"AiTempChanged_Off",
|
||||||
|
"AiTemp_240",
|
||||||
|
"Volume_100",
|
||||||
|
"StopAutoClean_Idle",
|
||||||
|
"DiagnosisAI_Off",
|
||||||
|
"ProgressDiagnosisAI_0",
|
||||||
|
"ResultDiagnosisAI_Normal",
|
||||||
|
"ExtendOptionCode_230024",
|
||||||
|
"UpdateAllow_NotAllowed",
|
||||||
|
"DurationOn_120",
|
||||||
|
"OutdoorTemp_92",
|
||||||
|
"CoolCapa_50",
|
||||||
|
"WarmCapa_60",
|
||||||
|
"Autoclean_Off",
|
||||||
|
"WelcomeCoolingState_Off",
|
||||||
|
"WelcomeHeatingState_Off",
|
||||||
|
"OutdoorConnection_Connected"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/option/airpurify/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.option.airpurify"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.a"
|
||||||
|
],
|
||||||
|
"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.supportedStatus": [
|
||||||
|
"Start",
|
||||||
|
"Stop"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.progress": "0",
|
||||||
|
"x.com.samsung.da.settingStatus": "Off",
|
||||||
|
"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",
|
||||||
|
"otnStatus": "None",
|
||||||
|
"flashingProgress": "0",
|
||||||
|
"otnCompleteDate": "noHistory",
|
||||||
|
"scheduledTime": "None",
|
||||||
|
"swVersionInfo": {
|
||||||
|
"platform": "Tizen Lite",
|
||||||
|
"oneUiVersion": "7.0 Air conditioner",
|
||||||
|
"osVersion": "4.0"
|
||||||
|
},
|
||||||
|
"otnList": [
|
||||||
|
{
|
||||||
|
"type": "WIFI",
|
||||||
|
"modelId": "ARA-KR-TP1-25-ARXX00",
|
||||||
|
"versions": [
|
||||||
|
"11260401"
|
||||||
|
],
|
||||||
|
"visVersion": "260401"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "845210267841FFFFFFFF",
|
||||||
|
"versions": [
|
||||||
|
"24092500",
|
||||||
|
"FFFFFFFF"
|
||||||
|
],
|
||||||
|
"visVersion": "240925"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "045210267841FFFFFFFF",
|
||||||
|
"versions": [
|
||||||
|
"24092500",
|
||||||
|
"FFFFFFFF"
|
||||||
|
],
|
||||||
|
"visVersion": "240925"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "04521026694110263641",
|
||||||
|
"versions": [
|
||||||
|
"24092600",
|
||||||
|
"10001200"
|
||||||
|
],
|
||||||
|
"visVersion": "240926"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "FGCAN3",
|
||||||
|
"versions": [
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"visVersion": "999999"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "845210266941FFFFFFFF",
|
||||||
|
"versions": [
|
||||||
|
"24092600",
|
||||||
|
"FFFFFFFF"
|
||||||
|
],
|
||||||
|
"visVersion": "240926"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/personality/presence/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/power/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.power": "On",
|
||||||
|
"operationNumber": "9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "false",
|
||||||
|
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/remotedatacontrol/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.status": "Off",
|
||||||
|
"x.com.samsung.da.connectionStatus": "Disconnected"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/remotetemperature/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.temperature": "",
|
||||||
|
"x.com.samsung.da.unit": "",
|
||||||
|
"x.com.samsung.da.error": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/reserverulesets/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.sets": "AEFFFFFFFF101E121EFFFF101EFFFF000001001F00010000001F0000000000FFFF1E00",
|
||||||
|
"x.com.samsung.da.id": "RAC",
|
||||||
|
"x.com.samsung.da.version": "1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/selfcheck/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.start": "Cancel",
|
||||||
|
"x.com.samsung.da.status": "Ready",
|
||||||
|
"x.com.samsung.da.progress": "0",
|
||||||
|
"x.com.samsung.da.result": "NA",
|
||||||
|
"x.com.samsung.da.supportedActions": [
|
||||||
|
"Start",
|
||||||
|
"Cancel"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.error": [
|
||||||
|
"DA_ERROR_NONE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperature/control/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.increment": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperatures/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Temperature",
|
||||||
|
"x.com.samsung.da.current": "26.0",
|
||||||
|
"x.com.samsung.da.desired": "22.0",
|
||||||
|
"x.com.samsung.da.minimum": "16",
|
||||||
|
"x.com.samsung.da.maximum": "30",
|
||||||
|
"x.com.samsung.da.increment": "1",
|
||||||
|
"x.com.samsung.da.unit": "Celsius"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/timezone/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"timezoneid": "Asia/Amman",
|
||||||
|
"offset": "+03:00",
|
||||||
|
"DST": "OFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/welcome/temperature/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"operatingStatus": "None",
|
||||||
|
"requestId": "0000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wind/direction/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modes": "All",
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"Fix",
|
||||||
|
"Up_And_Low",
|
||||||
|
"Left_And_Right",
|
||||||
|
"All"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wind/strength/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modes": "1",
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modesName": [
|
||||||
|
"Auto",
|
||||||
|
"Low",
|
||||||
|
"Mid",
|
||||||
|
"High",
|
||||||
|
"Turbo"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wirelessinfo/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"macaddressWiFi": "**REDACTED**",
|
||||||
|
"macaddressBLE": "**REDACTED**",
|
||||||
|
"connectedApSsid": "**REDACTED**"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"model": "TP2X_RAC_20K",
|
||||||
|
"device_type": "airconditioner",
|
||||||
|
"source": "issue #37 diagnostics (scrubbed)",
|
||||||
|
"note": "Older/simpler RAC board reporting no oneUiVersion -- resolved via the '_RAC_' modelNum token fallback in for_device_by_model (unlike issue #38's TP1X board, which self-reports oneUiVersion)."
|
||||||
|
},
|
||||||
|
"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-22T12:42:46",
|
||||||
|
"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-22T12:42:46",
|
||||||
|
"x.com.samsung.da.state": "Deleted"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/availablecontrolsets/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.sets": "000000A0012C01610249040F0000",
|
||||||
|
"x.com.samsung.da.id": "RAC",
|
||||||
|
"x.com.samsung.da.version": "1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/configuration/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.region": "0000000000",
|
||||||
|
"x.com.samsung.da.airconOptionList": [
|
||||||
|
"SingleCommand_1",
|
||||||
|
"HOMECARE_WIZARD_V2",
|
||||||
|
"PRODUCT_GLOBAL",
|
||||||
|
"AI_RAC_GLOBAL_HEATPUMP_3.0",
|
||||||
|
"AI_3.0",
|
||||||
|
"Auto_To_AI",
|
||||||
|
"AI_Heat"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/energy/consumption/0",
|
||||||
|
"rep": {
|
||||||
|
"power": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/energy/consumption/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.instantaneousPower": "0.000000",
|
||||||
|
"x.com.samsung.da.cumulativePower": "717060",
|
||||||
|
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||||
|
"x.com.samsung.da.cumulativePowerType": "total",
|
||||||
|
"x.com.samsung.da.instantaneousPowerUnit": "W"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/file/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.timeoffset": "+03:00",
|
||||||
|
"x.com.samsung.supprtedtype": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/filter/airdustfilter/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.filterUsage": "39",
|
||||||
|
"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/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.humidity": "0",
|
||||||
|
"x.com.samsung.da.fivepercentHumidity": "43"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modelNum": "TP2X_RAC_20K|10229641|60010523001511014600003200800000",
|
||||||
|
"x.com.samsung.da.description": "TP2X_RAC_20K",
|
||||||
|
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
|
||||||
|
"x.com.samsung.da.diagLogType": [
|
||||||
|
"errCode",
|
||||||
|
"dump"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.diagDumpType": "file",
|
||||||
|
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||||
|
"x.com.samsung.da.diagMnid": "0AJT",
|
||||||
|
"x.com.samsung.da.diagSetupid": "010",
|
||||||
|
"x.com.samsung.da.diagMinVersion": "1.0",
|
||||||
|
"x.com.samsung.da.serialNumOption": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Software",
|
||||||
|
"x.com.samsung.da.number": "02273A260324",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.number": "102296A23012700",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "2",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/keepnormalstate/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.keepnormal": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
"Auto",
|
||||||
|
"Cool",
|
||||||
|
"Dry",
|
||||||
|
"Wind",
|
||||||
|
"Heat",
|
||||||
|
"HOMECARE_WIZARD_V2"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modes": [
|
||||||
|
"Cool"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.options": [
|
||||||
|
"Sleep_16",
|
||||||
|
"ArtificialWorking_Off",
|
||||||
|
"ComfortAICooling_Off",
|
||||||
|
"MotionState_Normal",
|
||||||
|
"AiTempChanged_Off",
|
||||||
|
"AiTemp_240",
|
||||||
|
"OutdoorTemp_85",
|
||||||
|
"CoolCapa_35",
|
||||||
|
"WarmCapa_35",
|
||||||
|
"Light_Off",
|
||||||
|
"Volume_100",
|
||||||
|
"StopAutoClean_Idle",
|
||||||
|
"Autoclean_Off",
|
||||||
|
"DiagnosisAI_Off",
|
||||||
|
"ProgressDiagnosisAI_1",
|
||||||
|
"ResultDiagnosisAI_Normal",
|
||||||
|
"MotionMode_PowerSave",
|
||||||
|
"OptionCode_56376",
|
||||||
|
"ExtendOptionCode_230157",
|
||||||
|
"RacInfo_None",
|
||||||
|
"UpdateAllow_NotAllowed",
|
||||||
|
"DurationOn_50",
|
||||||
|
"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": "TP2X_RAC_20K",
|
||||||
|
"versions": [
|
||||||
|
"11260324"
|
||||||
|
],
|
||||||
|
"visVersion": "260324"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "045210229641FFFFFFFF",
|
||||||
|
"versions": [
|
||||||
|
"23012700",
|
||||||
|
"FFFFFFFF"
|
||||||
|
],
|
||||||
|
"visVersion": "230127"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "04521022954110229941",
|
||||||
|
"versions": [
|
||||||
|
"21092700",
|
||||||
|
"10000300"
|
||||||
|
],
|
||||||
|
"visVersion": "210927"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperature/control/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.increment": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperatures/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Temperature",
|
||||||
|
"x.com.samsung.da.desired": "22.0",
|
||||||
|
"x.com.samsung.da.current": "26.0",
|
||||||
|
"x.com.samsung.da.maximum": "30",
|
||||||
|
"x.com.samsung.da.minimum": "16",
|
||||||
|
"x.com.samsung.da.increment": "1.0",
|
||||||
|
"x.com.samsung.da.unit": "Celsius"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/timezone/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"timezoneid": "Asia/Amman",
|
||||||
|
"offset": "+03:00",
|
||||||
|
"DST": "OFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wind/direction/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modes": "Left_And_Right",
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"Fix",
|
||||||
|
"All",
|
||||||
|
"Up_And_Low",
|
||||||
|
"Left_And_Right"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wind/strength/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modes": "2",
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modesName": [
|
||||||
|
"Auto",
|
||||||
|
"Low",
|
||||||
|
"Mid",
|
||||||
|
"High",
|
||||||
|
"Turbo"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+1
@@ -8,6 +8,7 @@
|
|||||||
"climate",
|
"climate",
|
||||||
"diagnosis_status",
|
"diagnosis_status",
|
||||||
"energy_kwh",
|
"energy_kwh",
|
||||||
|
"energy_saved_kwh",
|
||||||
"power_watts"
|
"power_watts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"state_keys": [
|
||||||
|
"air_filter_status",
|
||||||
|
"air_filter_usage",
|
||||||
|
"air_purify",
|
||||||
|
"alarm_code",
|
||||||
|
"auto_clean",
|
||||||
|
"climate",
|
||||||
|
"display_light",
|
||||||
|
"energy_kwh",
|
||||||
|
"energy_saved_kwh",
|
||||||
|
"firmware_update",
|
||||||
|
"mute_once",
|
||||||
|
"selfcheck_error",
|
||||||
|
"selfcheck_result",
|
||||||
|
"selfcheck_status"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"state_keys": [
|
||||||
|
"air_filter_status",
|
||||||
|
"air_filter_usage",
|
||||||
|
"air_purify",
|
||||||
|
"alarm_code",
|
||||||
|
"auto_clean",
|
||||||
|
"climate",
|
||||||
|
"current_limit_enabled",
|
||||||
|
"current_limit_level",
|
||||||
|
"display_light",
|
||||||
|
"energy_kwh",
|
||||||
|
"energy_saved_kwh",
|
||||||
|
"firmware_update",
|
||||||
|
"mute_once",
|
||||||
|
"selfcheck_error",
|
||||||
|
"selfcheck_result",
|
||||||
|
"selfcheck_status"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"state_keys": [
|
||||||
|
"air_filter_status",
|
||||||
|
"air_filter_usage",
|
||||||
|
"alarm_code",
|
||||||
|
"auto_clean",
|
||||||
|
"climate",
|
||||||
|
"energy_kwh",
|
||||||
|
"firmware_update",
|
||||||
|
"mute_once",
|
||||||
|
"power_watts"
|
||||||
|
]
|
||||||
|
}
|
||||||
+2
@@ -8,6 +8,7 @@
|
|||||||
"delay_start_hours",
|
"delay_start_hours",
|
||||||
"diagnosis_status",
|
"diagnosis_status",
|
||||||
"energy_kwh",
|
"energy_kwh",
|
||||||
|
"energy_saved_kwh",
|
||||||
"finish_time",
|
"finish_time",
|
||||||
"firmware_update",
|
"firmware_update",
|
||||||
"heated_dry",
|
"heated_dry",
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
"samsung_dishwasher_diagnosis_start",
|
"samsung_dishwasher_diagnosis_start",
|
||||||
"samsung_dishwasher_diagnosis_status",
|
"samsung_dishwasher_diagnosis_status",
|
||||||
"samsung_dishwasher_energy_kwh",
|
"samsung_dishwasher_energy_kwh",
|
||||||
|
"samsung_dishwasher_energy_saved_kwh",
|
||||||
"samsung_dishwasher_finish_time",
|
"samsung_dishwasher_finish_time",
|
||||||
"samsung_dishwasher_firmware_update",
|
"samsung_dishwasher_firmware_update",
|
||||||
"samsung_dishwasher_heated_dry",
|
"samsung_dishwasher_heated_dry",
|
||||||
|
|||||||
Vendored
+39
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"state_keys": [
|
||||||
|
"alarm_code",
|
||||||
|
"burner_0_hot_surface",
|
||||||
|
"burner_0_power_level",
|
||||||
|
"burner_0_state",
|
||||||
|
"burner_1_hot_surface",
|
||||||
|
"burner_1_power_level",
|
||||||
|
"burner_1_state",
|
||||||
|
"burner_2_hot_surface",
|
||||||
|
"burner_2_power_level",
|
||||||
|
"burner_2_state",
|
||||||
|
"burner_3_hot_surface",
|
||||||
|
"burner_3_power_level",
|
||||||
|
"burner_3_state",
|
||||||
|
"child_lock",
|
||||||
|
"cloud_connected",
|
||||||
|
"cook_time",
|
||||||
|
"cooktop_safety_shutoff_enabled",
|
||||||
|
"cooktop_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"
|
||||||
|
]
|
||||||
|
}
|
||||||
+12
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"state_keys": [
|
"state_keys": [
|
||||||
|
"ai_energy_level",
|
||||||
"alarm_code",
|
"alarm_code",
|
||||||
"auto_door_opener",
|
"auto_door_opener",
|
||||||
"autofill",
|
"autofill",
|
||||||
@@ -16,6 +17,9 @@
|
|||||||
"door_cooler_open",
|
"door_cooler_open",
|
||||||
"door_freezer_open",
|
"door_freezer_open",
|
||||||
"energy_kwh",
|
"energy_kwh",
|
||||||
|
"energy_last_month_kwh",
|
||||||
|
"energy_saved_kwh",
|
||||||
|
"energy_this_month_kwh",
|
||||||
"filter_status",
|
"filter_status",
|
||||||
"filter_usage",
|
"filter_usage",
|
||||||
"firmware_update",
|
"firmware_update",
|
||||||
@@ -31,15 +35,18 @@
|
|||||||
"icemaker_two_type",
|
"icemaker_two_type",
|
||||||
"night_end",
|
"night_end",
|
||||||
"night_start",
|
"night_start",
|
||||||
|
"power_energy_kwh",
|
||||||
"power_watts",
|
"power_watts",
|
||||||
"rapid_freezing",
|
"rapid_freezing",
|
||||||
"rapid_fridge",
|
"rapid_fridge",
|
||||||
"sabbath_mode",
|
"sabbath_mode",
|
||||||
|
"selfcheck_error",
|
||||||
"selfcheck_result",
|
"selfcheck_result",
|
||||||
"selfcheck_status",
|
"selfcheck_status",
|
||||||
"welcome_lighting"
|
"welcome_lighting"
|
||||||
],
|
],
|
||||||
"discovery_unique_ids": [
|
"discovery_unique_ids": [
|
||||||
|
"samsung_refrigerator_ai_energy_level",
|
||||||
"samsung_refrigerator_alarm_code",
|
"samsung_refrigerator_alarm_code",
|
||||||
"samsung_refrigerator_auto_door_opener",
|
"samsung_refrigerator_auto_door_opener",
|
||||||
"samsung_refrigerator_autofill",
|
"samsung_refrigerator_autofill",
|
||||||
@@ -56,6 +63,9 @@
|
|||||||
"samsung_refrigerator_door_cooler_open",
|
"samsung_refrigerator_door_cooler_open",
|
||||||
"samsung_refrigerator_door_freezer_open",
|
"samsung_refrigerator_door_freezer_open",
|
||||||
"samsung_refrigerator_energy_kwh",
|
"samsung_refrigerator_energy_kwh",
|
||||||
|
"samsung_refrigerator_energy_last_month_kwh",
|
||||||
|
"samsung_refrigerator_energy_saved_kwh",
|
||||||
|
"samsung_refrigerator_energy_this_month_kwh",
|
||||||
"samsung_refrigerator_filter_status",
|
"samsung_refrigerator_filter_status",
|
||||||
"samsung_refrigerator_filter_usage",
|
"samsung_refrigerator_filter_usage",
|
||||||
"samsung_refrigerator_firmware_update",
|
"samsung_refrigerator_firmware_update",
|
||||||
@@ -71,10 +81,12 @@
|
|||||||
"samsung_refrigerator_icemaker_two_type",
|
"samsung_refrigerator_icemaker_two_type",
|
||||||
"samsung_refrigerator_night_end",
|
"samsung_refrigerator_night_end",
|
||||||
"samsung_refrigerator_night_start",
|
"samsung_refrigerator_night_start",
|
||||||
|
"samsung_refrigerator_power_energy_kwh",
|
||||||
"samsung_refrigerator_power_watts",
|
"samsung_refrigerator_power_watts",
|
||||||
"samsung_refrigerator_rapid_freezing",
|
"samsung_refrigerator_rapid_freezing",
|
||||||
"samsung_refrigerator_rapid_fridge",
|
"samsung_refrigerator_rapid_fridge",
|
||||||
"samsung_refrigerator_sabbath_mode",
|
"samsung_refrigerator_sabbath_mode",
|
||||||
|
"samsung_refrigerator_selfcheck_error",
|
||||||
"samsung_refrigerator_selfcheck_result",
|
"samsung_refrigerator_selfcheck_result",
|
||||||
"samsung_refrigerator_selfcheck_start",
|
"samsung_refrigerator_selfcheck_start",
|
||||||
"samsung_refrigerator_selfcheck_status",
|
"samsung_refrigerator_selfcheck_status",
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"state_keys": [
|
||||||
|
"alarm_code",
|
||||||
|
"autofill",
|
||||||
|
"cooler_setpoint",
|
||||||
|
"cooler_temperature",
|
||||||
|
"defrost_active",
|
||||||
|
"defrost_delay",
|
||||||
|
"diagnosis_status",
|
||||||
|
"door_cooler_open",
|
||||||
|
"door_freezer_open",
|
||||||
|
"energy_last_month_kwh",
|
||||||
|
"energy_this_month_kwh",
|
||||||
|
"filter_status",
|
||||||
|
"filter_usage",
|
||||||
|
"firmware_update",
|
||||||
|
"freezer_setpoint",
|
||||||
|
"freezer_temperature",
|
||||||
|
"icemaker_one_enabled",
|
||||||
|
"icemaker_one_making_status",
|
||||||
|
"pantry_zone_mode",
|
||||||
|
"power_energy_kwh",
|
||||||
|
"power_watts",
|
||||||
|
"rapid_freezing",
|
||||||
|
"rapid_fridge",
|
||||||
|
"sabbath_mode"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"state_keys": [
|
||||||
|
"ai_energy_level",
|
||||||
|
"alarm_code",
|
||||||
|
"autofill",
|
||||||
|
"cooler_setpoint",
|
||||||
|
"cooler_temperature",
|
||||||
|
"defrost_active",
|
||||||
|
"defrost_delay",
|
||||||
|
"door_cooler_open",
|
||||||
|
"door_cvroom_open",
|
||||||
|
"door_freezer_open",
|
||||||
|
"energy_kwh",
|
||||||
|
"energy_last_month_kwh",
|
||||||
|
"energy_saved_kwh",
|
||||||
|
"energy_this_month_kwh",
|
||||||
|
"filter_status",
|
||||||
|
"filter_usage",
|
||||||
|
"firmware_update",
|
||||||
|
"flex_zone_mode",
|
||||||
|
"freezer_setpoint",
|
||||||
|
"freezer_temperature",
|
||||||
|
"icemaker_one_enabled",
|
||||||
|
"icemaker_one_making_status",
|
||||||
|
"icemaker_two_enabled",
|
||||||
|
"icemaker_two_making_status",
|
||||||
|
"power_energy_kwh",
|
||||||
|
"power_watts",
|
||||||
|
"rapid_freezing",
|
||||||
|
"rapid_fridge",
|
||||||
|
"sabbath_mode",
|
||||||
|
"selfcheck_error",
|
||||||
|
"selfcheck_result",
|
||||||
|
"selfcheck_status"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"state_keys": [
|
||||||
|
"alarm_code",
|
||||||
|
"cooler_setpoint",
|
||||||
|
"cooler_temperature",
|
||||||
|
"defrost_active",
|
||||||
|
"defrost_delay",
|
||||||
|
"diagnosis_status",
|
||||||
|
"door_cooler_open",
|
||||||
|
"door_cvroom_open",
|
||||||
|
"door_freezer_open",
|
||||||
|
"energy_kwh",
|
||||||
|
"energy_last_month_kwh",
|
||||||
|
"energy_this_month_kwh",
|
||||||
|
"filter_status",
|
||||||
|
"filter_usage",
|
||||||
|
"firmware_update",
|
||||||
|
"flex_zone_mode",
|
||||||
|
"freezer_setpoint",
|
||||||
|
"freezer_temperature",
|
||||||
|
"icemaker_one_enabled",
|
||||||
|
"icemaker_one_making_status",
|
||||||
|
"power_energy_kwh",
|
||||||
|
"power_watts",
|
||||||
|
"rapid_freezing",
|
||||||
|
"rapid_fridge",
|
||||||
|
"sabbath_mode"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+5
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"state_keys": [
|
"state_keys": [
|
||||||
|
"ai_energy_level",
|
||||||
"alarm_code",
|
"alarm_code",
|
||||||
|
"bubble_soak",
|
||||||
"buzzer_sound",
|
"buzzer_sound",
|
||||||
"child_lock",
|
"child_lock",
|
||||||
"cycle",
|
"cycle",
|
||||||
@@ -11,11 +13,14 @@
|
|||||||
"drum_clean_cycles_remaining",
|
"drum_clean_cycles_remaining",
|
||||||
"drum_clean_last_cleaned",
|
"drum_clean_last_cleaned",
|
||||||
"energy_kwh",
|
"energy_kwh",
|
||||||
|
"energy_saved_kwh",
|
||||||
"finish_time",
|
"finish_time",
|
||||||
"firmware_update",
|
"firmware_update",
|
||||||
|
"intensive",
|
||||||
"job_beginning_status",
|
"job_beginning_status",
|
||||||
"machine_state",
|
"machine_state",
|
||||||
"power_switch",
|
"power_switch",
|
||||||
|
"pre_wash",
|
||||||
"progress",
|
"progress",
|
||||||
"progress_percentage",
|
"progress_percentage",
|
||||||
"remote_control",
|
"remote_control",
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"state_keys": [
|
||||||
|
"alarm_code",
|
||||||
|
"bubble_soak",
|
||||||
|
"child_lock",
|
||||||
|
"cycle",
|
||||||
|
"cycle_active",
|
||||||
|
"delay_start_hours",
|
||||||
|
"detergent_low",
|
||||||
|
"diagnosis_status",
|
||||||
|
"drum_clean_cycles_remaining",
|
||||||
|
"dry_level",
|
||||||
|
"energy_kwh",
|
||||||
|
"finish_time",
|
||||||
|
"firmware_update",
|
||||||
|
"intensive",
|
||||||
|
"job_beginning_status",
|
||||||
|
"machine_state",
|
||||||
|
"power_switch",
|
||||||
|
"pre_wash",
|
||||||
|
"progress",
|
||||||
|
"progress_percentage",
|
||||||
|
"remote_control",
|
||||||
|
"rinse_cycles",
|
||||||
|
"softener_low",
|
||||||
|
"spin_speed",
|
||||||
|
"wash_temperature"
|
||||||
|
]
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"state_keys": [
|
||||||
|
"alarm_code",
|
||||||
|
"child_lock",
|
||||||
|
"cycle_active",
|
||||||
|
"delay_start_hours",
|
||||||
|
"detergent_low",
|
||||||
|
"diagnosis_status",
|
||||||
|
"drum_clean_cycles_remaining",
|
||||||
|
"energy_kwh",
|
||||||
|
"finish_time",
|
||||||
|
"job_beginning_status",
|
||||||
|
"machine_state",
|
||||||
|
"power_switch",
|
||||||
|
"progress",
|
||||||
|
"progress_percentage",
|
||||||
|
"remote_control",
|
||||||
|
"rinse_cycles",
|
||||||
|
"spin_speed",
|
||||||
|
"wash_temperature"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+466
@@ -0,0 +1,466 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"model": "TP1X_DA-KS-RANGE-0102X (NSI6DG9100SRAA)",
|
||||||
|
"device_type": "range",
|
||||||
|
"source": "issue #44 diagnostics (scrubbed)",
|
||||||
|
"note": "Range/cooktop-oven combo, first dump of this device type. 4 burners reported (numberOfBurners=4)."
|
||||||
|
},
|
||||||
|
"device0": [
|
||||||
|
{
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.devcol",
|
||||||
|
"oic.wk.col"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.ll",
|
||||||
|
"oic.if.b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/otninformation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.target": "",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "false",
|
||||||
|
"flashingProgress": "",
|
||||||
|
"otnStatus": "None",
|
||||||
|
"otnList": [
|
||||||
|
{
|
||||||
|
"type": "WIFI",
|
||||||
|
"modelId": "AKS-WW-TP1-23-OVEN-3",
|
||||||
|
"versions": [
|
||||||
|
"40241114"
|
||||||
|
],
|
||||||
|
"visVersion": "241114"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "07174047694140477041",
|
||||||
|
"versions": [
|
||||||
|
"24052400",
|
||||||
|
"24012300"
|
||||||
|
],
|
||||||
|
"visVersion": "240524"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "071760141141FFFFFFFF",
|
||||||
|
"versions": [
|
||||||
|
"24010500",
|
||||||
|
"FFFFFFFF"
|
||||||
|
],
|
||||||
|
"visVersion": "240105"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "071740476841FFFFFFFF",
|
||||||
|
"versions": [
|
||||||
|
"23121600",
|
||||||
|
"FFFFFFFF"
|
||||||
|
],
|
||||||
|
"visVersion": "231216"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Micom",
|
||||||
|
"modelId": "07174047484140474941",
|
||||||
|
"versions": [
|
||||||
|
"24020500",
|
||||||
|
"24012600"
|
||||||
|
],
|
||||||
|
"visVersion": "240205"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/connected/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.connected": "On"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/doors/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.doors"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.s"
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modelNum": "TP1X_DA-KS-RANGE-0102X|40476941|5001011E041811010A00000000000000",
|
||||||
|
"x.com.samsung.da.description": "NI9100D-/AA0",
|
||||||
|
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Software",
|
||||||
|
"x.com.samsung.da.number": "24111400",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.number": "04769A24052400, 04770A24012300",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "2",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.number": "01411A24010500",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "3",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.number": "04768A23121600",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "4",
|
||||||
|
"x.com.samsung.da.description": "Version",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.number": "04748A24020500, 04749A24012600",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||||
|
"x.com.samsung.da.diagLogType": [
|
||||||
|
"errCode",
|
||||||
|
"dump"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.diagDumpType": "file",
|
||||||
|
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||||
|
"x.com.samsung.da.diagMnid": "0AJT",
|
||||||
|
"x.com.samsung.da.diagSetupid": "KR3",
|
||||||
|
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||||
|
"x.com.samsung.da.diagTsId": "DA01"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.mode"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.a"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"Bake",
|
||||||
|
"Broil",
|
||||||
|
"KeepWarm",
|
||||||
|
"BreadProof",
|
||||||
|
"SelfClean",
|
||||||
|
"SteamClean"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modes": [
|
||||||
|
"NoOperation"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modeStateList": [
|
||||||
|
{
|
||||||
|
"mode": "Bake",
|
||||||
|
"state": "Fixed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "Broil",
|
||||||
|
"state": "Show"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "KeepWarm",
|
||||||
|
"state": "Show"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "BreadProof",
|
||||||
|
"state": "Show"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "SelfClean",
|
||||||
|
"state": "Fixed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "SteamClean",
|
||||||
|
"state": "Fixed"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.options": [
|
||||||
|
"DeviceType_NI9100D-/AA0",
|
||||||
|
"SettingPossible_5",
|
||||||
|
"TimeAutoSync_On",
|
||||||
|
"UpperLamp_On",
|
||||||
|
"TimeSystem_12",
|
||||||
|
"Sound_On",
|
||||||
|
"AdjustingTemp_0",
|
||||||
|
"Sabbath_Off",
|
||||||
|
"EnergySaving_On"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.defaultMode": "Bake",
|
||||||
|
"x.com.samsung.da.modeSpec": "[{\"mode\":\"Bake\",\"version\":\"0101\",\"default\":\"Default\",\"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\":\"40\",\"probeMaxC\":\"90\",\"probeDefaultC\":\"65\",\"probeMinF\":\"100\",\"probeMaxF\":\"200\",\"probeDefaultF\":\"150\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"5\",\"tempIntervalF\":\"5\",\"probeIntervalC\":\"1\",\"probeIntervalF\":\"1\"},{\"mode\":\"Broil\",\"version\":\"0101\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"61442\",\"tempMaxC\":\"61441\",\"tempDefaultC\":\"61441\",\"tempListLengthC\":\"2\",\"tempListDataC\":[\"61441\",\"61442\"],\"tempMinF\":\"61442\",\"tempMaxF\":\"61441\",\"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\":\"NotSupported\",\"tempIntervalF\":\"NotSupported\",\"probeIntervalC\":\"NotSupported\",\"probeIntervalF\":\"NotSupported\"},{\"mode\":\"KeepWarm\",\"version\":\"0101\",\"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\":\"NotSupported\",\"tempIntervalF\":\"NotSupported\",\"probeIntervalC\":\"NotSupported\",\"probeIntervalF\":\"NotSupported\"},{\"mode\":\"BreadProof\",\"version\":\"0101\",\"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\":\"NotSupported\",\"tempIntervalF\":\"NotSupported\",\"probeIntervalC\":\"NotSupported\",\"probeIntervalF\":\"NotSupported\"},{\"mode\":\"SelfClean\",\"version\":\"0101\",\"default\":\"Normal\",\"control\":\"NotSupported\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"NotSupported\",\"tempIntervalF\":\"NotSupported\",\"probeIntervalC\":\"NotSupported\",\"probeIntervalF\":\"NotSupported\"},{\"mode\":\"SteamClean\",\"version\":\"0101\",\"default\":\"Normal\",\"control\":\"NotSupported\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"NotSupported\",\"tempIntervalF\":\"NotSupported\",\"probeIntervalC\":\"NotSupported\",\"probeIntervalF\":\"NotSupported\"}]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/operational/state/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.operation"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.a"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.state": "Ready",
|
||||||
|
"causeSource.state": "SETB_Ready",
|
||||||
|
"x.com.samsung.da.operationTime": "00:00:00",
|
||||||
|
"x.com.samsung.da.remainingTime": "00:00:00",
|
||||||
|
"x.com.samsung.da.progressPercentage": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "2026-07-22T23:55:41"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Alarm",
|
||||||
|
"x.com.samsung.da.alarmType": "Device",
|
||||||
|
"x.com.samsung.da.code": "CT_E_OFF",
|
||||||
|
"x.com.samsung.da.triggeredTime": "2026-07-22T23:55:41"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/oven/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.oven"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.s"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.state": "Ready"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperatures/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.da.temperatures"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.a"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Temperature",
|
||||||
|
"x.com.samsung.da.desired": "0",
|
||||||
|
"x.com.samsung.da.current": "175",
|
||||||
|
"x.com.samsung.da.increment": "5",
|
||||||
|
"x.com.samsung.da.unit": "Fahrenheit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/timezone/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"timezoneid": "America/Chicago",
|
||||||
|
"offset": "-05:00",
|
||||||
|
"DST": "ON"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/oven/spec/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"cavityInfo": {
|
||||||
|
"numberOfCavity": 1,
|
||||||
|
"type": "single",
|
||||||
|
"serviceMainCavity": "single",
|
||||||
|
"cavityList": [
|
||||||
|
{
|
||||||
|
"name": "single",
|
||||||
|
"flexSupport": false,
|
||||||
|
"single": {
|
||||||
|
"resourceId": 0,
|
||||||
|
"supportedFeatureList": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/kidslock/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.kidsLock": "Ready"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/power/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.power": "On"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/remotectrl/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.remoteControlEnabled": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/cooktop/settings/status/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"safetyAlert": {
|
||||||
|
"state": "on",
|
||||||
|
"settingTime": 1200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/cooktop/spec/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"supportedFeatureList": [
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"numberOfBurners": 4,
|
||||||
|
"supportedBurnerList": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"supportedPowerLevelList": [
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
|
"boost",
|
||||||
|
"0",
|
||||||
|
"simmer"
|
||||||
|
],
|
||||||
|
"supportedRecipeProtocolVersion": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/cooktop/status/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"power": "off",
|
||||||
|
"childLock": "off",
|
||||||
|
"operationState": "ready",
|
||||||
|
"additionalStateList": [
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"burnerList": [
|
||||||
|
{
|
||||||
|
"burnerNumber": 0,
|
||||||
|
"mode": "normal",
|
||||||
|
"operationState": "ready",
|
||||||
|
"powerLevel": "0",
|
||||||
|
"hotSurfaceState": "normal",
|
||||||
|
"panDetection": true,
|
||||||
|
"timer": {
|
||||||
|
"operationState": "ready",
|
||||||
|
"cookingTime": 0,
|
||||||
|
"remainingTime": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"burnerNumber": 1,
|
||||||
|
"mode": "normal",
|
||||||
|
"operationState": "ready",
|
||||||
|
"powerLevel": "0",
|
||||||
|
"hotSurfaceState": "normal",
|
||||||
|
"panDetection": true,
|
||||||
|
"timer": {
|
||||||
|
"operationState": "ready",
|
||||||
|
"cookingTime": 0,
|
||||||
|
"remainingTime": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"burnerNumber": 2,
|
||||||
|
"mode": "normal",
|
||||||
|
"operationState": "ready",
|
||||||
|
"powerLevel": "0",
|
||||||
|
"hotSurfaceState": "normal",
|
||||||
|
"panDetection": true,
|
||||||
|
"timer": {
|
||||||
|
"operationState": "ready",
|
||||||
|
"cookingTime": 0,
|
||||||
|
"remainingTime": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"burnerNumber": 3,
|
||||||
|
"mode": "normal",
|
||||||
|
"operationState": "ready",
|
||||||
|
"powerLevel": "0",
|
||||||
|
"hotSurfaceState": "normal",
|
||||||
|
"panDetection": true,
|
||||||
|
"timer": {
|
||||||
|
"operationState": "ready",
|
||||||
|
"cookingTime": 0,
|
||||||
|
"remainingTime": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wirelessinfo/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"macaddressWiFi": "**REDACTED**",
|
||||||
|
"macaddressBLE": "**REDACTED**"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/quickcontrol/info/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"supportedVersion": "1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
{
|
||||||
|
"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": "/temperatures/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Freezer",
|
||||||
|
"x.com.samsung.da.desired": "-21",
|
||||||
|
"x.com.samsung.da.current": "-21",
|
||||||
|
"x.com.samsung.da.maximum": "-15",
|
||||||
|
"x.com.samsung.da.minimum": "-23",
|
||||||
|
"x.com.samsung.da.unit": "Celsius"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Fridge",
|
||||||
|
"x.com.samsung.da.desired": "2",
|
||||||
|
"x.com.samsung.da.current": "2",
|
||||||
|
"x.com.samsung.da.maximum": "7",
|
||||||
|
"x.com.samsung.da.minimum": "1",
|
||||||
|
"x.com.samsung.da.unit": "Celsius"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperature/current/freezer/0",
|
||||||
|
"rep": {
|
||||||
|
"range": [
|
||||||
|
-23.0,
|
||||||
|
-15.0
|
||||||
|
],
|
||||||
|
"units": "C",
|
||||||
|
"temperature": -21.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperature/desired/freezer/0",
|
||||||
|
"rep": {
|
||||||
|
"range": [
|
||||||
|
-23.0,
|
||||||
|
-15.0
|
||||||
|
],
|
||||||
|
"units": "C",
|
||||||
|
"temperature": -21.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperature/current/cooler/0",
|
||||||
|
"rep": {
|
||||||
|
"range": [
|
||||||
|
1.0,
|
||||||
|
7.0
|
||||||
|
],
|
||||||
|
"units": "C",
|
||||||
|
"temperature": 2.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperature/desired/cooler/0",
|
||||||
|
"rep": {
|
||||||
|
"range": [
|
||||||
|
1.0,
|
||||||
|
7.0
|
||||||
|
],
|
||||||
|
"units": "C",
|
||||||
|
"temperature": 2.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/diagnosis/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/energy/consumption/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.cumulativeConsumption": "0",
|
||||||
|
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||||
|
"x.com.samsung.da.instantaneousPower": "0",
|
||||||
|
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||||
|
"x.com.samsung.da.monthlyConsumption": "86000",
|
||||||
|
"x.com.samsung.da.thismonthlyConsumption": "64026"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"HOMECARE_WIZARD_V2",
|
||||||
|
"ENERGY_REPORT_MODEL",
|
||||||
|
"18K_REF_OUTDOOR_CONTROL_V2",
|
||||||
|
"SUPPORT_SABBATH_CONTROL"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modes": [
|
||||||
|
"WATERFILTER_ENABLE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/0",
|
||||||
|
"rep": {
|
||||||
|
"supportedModes": [
|
||||||
|
"HOMECARE_WIZARD_V2",
|
||||||
|
"ENERGY_REPORT_MODEL",
|
||||||
|
"18K_REF_OUTDOOR_CONTROL_V2",
|
||||||
|
"SUPPORT_SABBATH_CONTROL"
|
||||||
|
],
|
||||||
|
"modes": [
|
||||||
|
"WATERFILTER_ENABLE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/defrost/block/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"DEFROST_BLOCK_ON",
|
||||||
|
"DEFROST_BLOCK_OFF"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modes": [
|
||||||
|
"DEFROST_BLOCK_OFF"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/sabbath/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.sabbathMode": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/realtimenotiforclient/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.timeforshortnoti": "0",
|
||||||
|
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modelNum": "ARTIK051_REF_17K|00117141|0002074E051311200103000010000000",
|
||||||
|
"x.com.samsung.da.description": "ARTIK051_REF_17K",
|
||||||
|
"x.com.samsung.da.serialNum": "REDACTED",
|
||||||
|
"x.com.samsung.da.otnDUID": "REDACTED",
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "WiFi Module",
|
||||||
|
"x.com.samsung.da.type": "Software",
|
||||||
|
"x.com.samsung.da.number": "02144A201124",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Micom",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.number": "2003030C,FFFFFFFF",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/file/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.timeoffset": "-04:00",
|
||||||
|
"x.com.samsung.supprtedtype": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Door",
|
||||||
|
"x.com.samsung.da.openState": "Close"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/door/freezer/0",
|
||||||
|
"rep": {
|
||||||
|
"openState": "Close"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/door/cooler/0",
|
||||||
|
"rep": {
|
||||||
|
"openState": "Close"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/configuration/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.countryCode": "US",
|
||||||
|
"x.com.samsung.da.region": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/defrost/delay/0",
|
||||||
|
"rep": {
|
||||||
|
"value": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/defrost/delay/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.delayDefrost": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/defrost/reservation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Summer Season",
|
||||||
|
"x.com.samsung.da.startTime": "0000-05-01T15:00:00",
|
||||||
|
"x.com.samsung.da.period": "04:00:00",
|
||||||
|
"x.com.samsung.da.endTime": "0000-10-31T00:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Winter Season",
|
||||||
|
"x.com.samsung.da.startTime": "0000-11-01T06:00:00",
|
||||||
|
"x.com.samsung.da.period": "04:00:00",
|
||||||
|
"x.com.samsung.da.endTime": "0000-04-30T00:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/icemaker/status/0",
|
||||||
|
"rep": {
|
||||||
|
"status": "On"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/icemaker/status/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.iceMaker": "On"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/refrigeration/0",
|
||||||
|
"rep": {
|
||||||
|
"defrost": false,
|
||||||
|
"rapidFreeze": false,
|
||||||
|
"rapidCool": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/refrigeration/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.rapidFridge": "Off",
|
||||||
|
"x.com.samsung.da.rapidFreezing": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/drlc/0",
|
||||||
|
"rep": {
|
||||||
|
"DRLevel": 0,
|
||||||
|
"start": "00:00:00",
|
||||||
|
"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": "00:00:00",
|
||||||
|
"x.com.samsung.da.override": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/otninformation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.target": "Micom",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/filter/waterfilter/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.filterUsage": "100",
|
||||||
|
"x.com.samsung.da.filterUsageResolution": "1",
|
||||||
|
"x.com.samsung.da.filterStatus": "replace",
|
||||||
|
"x.com.samsung.da.filterResetType": [
|
||||||
|
"replaceable"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/autofill/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.autofill": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/icemaker/one/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.iceMaker.name": "ICE_MAKER",
|
||||||
|
"x.com.samsung.da.iceMaker.state": "On",
|
||||||
|
"x.com.samsung.da.iceMaker.type": "toggle",
|
||||||
|
"x.com.samsung.da.iceMaker.iceMakingStatus": "ICESTATUS_STOP",
|
||||||
|
"x.com.samsung.da.iceType.desired": "NORMAL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/status/pantry/one/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.controllable": "true",
|
||||||
|
"x.com.samsung.da.name": "COOL_SELECT_ZONE",
|
||||||
|
"x.com.samsung.da.mode": "FDR_DRINKS",
|
||||||
|
"x.com.samsung.da.supportedOptions": [
|
||||||
|
"FDR_WINE",
|
||||||
|
"FDR_DELI",
|
||||||
|
"FDR_DRINKS"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+2
-1
@@ -279,7 +279,8 @@
|
|||||||
"rep": {
|
"rep": {
|
||||||
"aiLevel": "1",
|
"aiLevel": "1",
|
||||||
"supportedAiLevel": [
|
"supportedAiLevel": [
|
||||||
"1"
|
"1",
|
||||||
|
"2"
|
||||||
],
|
],
|
||||||
"href": "/energy/ailevel/vs/0"
|
"href": "/energy/ailevel/vs/0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
{
|
||||||
|
"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": "/temperatures/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Freezer",
|
||||||
|
"x.com.samsung.da.desired": "0",
|
||||||
|
"x.com.samsung.da.current": "0",
|
||||||
|
"x.com.samsung.da.maximum": "5",
|
||||||
|
"x.com.samsung.da.minimum": "-8",
|
||||||
|
"x.com.samsung.da.unit": "Fahrenheit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Fridge",
|
||||||
|
"x.com.samsung.da.desired": "39",
|
||||||
|
"x.com.samsung.da.current": "39",
|
||||||
|
"x.com.samsung.da.maximum": "44",
|
||||||
|
"x.com.samsung.da.minimum": "34",
|
||||||
|
"x.com.samsung.da.unit": "Fahrenheit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"href": "/temperature/current/freezer/0", "rep": {"range": [-8.0, 5.0], "units": "F", "temperature": 0.0}},
|
||||||
|
{"href": "/temperature/desired/freezer/0", "rep": {"range": [-8.0, 5.0], "units": "F", "temperature": 0.0}},
|
||||||
|
{"href": "/temperature/current/cooler/0", "rep": {"range": [34.0, 44.0], "units": "F", "temperature": 39.0}},
|
||||||
|
{"href": "/temperature/desired/cooler/0", "rep": {"range": [34.0, 44.0], "units": "F", "temperature": 39.0}},
|
||||||
|
{
|
||||||
|
"href": "/selfcheck/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.status": "Ready",
|
||||||
|
"x.com.samsung.da.result": "Success",
|
||||||
|
"x.com.samsung.da.error": ["DA_ERROR_NONE"],
|
||||||
|
"x.com.samsung.da.supportedActions": ["Start"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/energy/consumption/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.cumulativeConsumption": "63",
|
||||||
|
"x.com.samsung.da.cumulativePower": "1831899",
|
||||||
|
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||||
|
"x.com.samsung.da.instantaneousPower": "3",
|
||||||
|
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||||
|
"x.com.samsung.da.monthlyConsumption": "52000",
|
||||||
|
"x.com.samsung.da.thismonthlyConsumption": "38900",
|
||||||
|
"x.com.samsung.da.cumulativeSavedPower": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"HOMECARE_WIZARD_V2",
|
||||||
|
"ENERGY_REPORT_MODEL",
|
||||||
|
"18K_REF_OUTDOOR_CONTROL_V2",
|
||||||
|
"SUPPORT_SABBATH_CONTROL"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modes": ["CVN_CONVERTIBLE_ZONE", "CV_FDR_WINE", "WATERFILTER_DISABLE"],
|
||||||
|
"x.com.samsung.da.supportedOptions": [
|
||||||
|
"CV_FDR_WINE",
|
||||||
|
"CV_FDR_DELI",
|
||||||
|
"CV_FDR_BEVERAGE",
|
||||||
|
"CV_FDR_MEAT",
|
||||||
|
"CV_FDR_SOFT_FREEZER"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/0",
|
||||||
|
"rep": {
|
||||||
|
"supportedModes": [
|
||||||
|
"HOMECARE_WIZARD_V2",
|
||||||
|
"ENERGY_REPORT_MODEL",
|
||||||
|
"18K_REF_OUTDOOR_CONTROL_V2",
|
||||||
|
"SUPPORT_SABBATH_CONTROL"
|
||||||
|
],
|
||||||
|
"modes": ["CVN_CONVERTIBLE_ZONE", "CV_FDR_WINE", "WATERFILTER_DISABLE"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/defrost/block/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.supportedModes": ["DEFROST_BLOCK_ON", "DEFROST_BLOCK_OFF"],
|
||||||
|
"x.com.samsung.da.modes": ["DEFROST_BLOCK_OFF"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"href": "/sabbath/vs/0", "rep": {"x.com.samsung.da.sabbathMode": "Off"}},
|
||||||
|
{
|
||||||
|
"href": "/realtimenotiforclient/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.timeforshortnoti": "0",
|
||||||
|
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modelNum": "TP1X_REF_21K|00148241|00020850031611204103000021010000",
|
||||||
|
"x.com.samsung.da.description": "TP1X_REF_21K",
|
||||||
|
"x.com.samsung.da.serialNum": "REDACTED",
|
||||||
|
"x.com.samsung.da.otnDUID": "REDACTED",
|
||||||
|
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
|
||||||
|
"x.com.samsung.da.diagLogType": ["errCode", "dump"],
|
||||||
|
"x.com.samsung.da.diagDumpType": "file",
|
||||||
|
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||||
|
"x.com.samsung.da.diagMnid": "0AJT",
|
||||||
|
"x.com.samsung.da.diagSetupid": "512",
|
||||||
|
"x.com.samsung.da.diagMinVersion": "1.0",
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "WiFi Module",
|
||||||
|
"x.com.samsung.da.type": "Software",
|
||||||
|
"x.com.samsung.da.number": "250420",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Micom",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.number": "23071923, FFFFFFFF",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"href": "/file/information/vs/0", "rep": {"x.com.samsung.timeoffset": "-05:00", "x.com.samsung.supprtedtype": 1}},
|
||||||
|
{
|
||||||
|
"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.id": "1", "x.com.samsung.da.description": "Door", "x.com.samsung.da.openState": "Close"},
|
||||||
|
{"x.com.samsung.da.id": "2", "x.com.samsung.da.description": "Door", "x.com.samsung.da.openState": "Close"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"href": "/door/freezer/0", "rep": {"openState": "Close"}},
|
||||||
|
{"href": "/door/cooler/0", "rep": {"openState": "Close"}},
|
||||||
|
{"href": "/door/cvroom/0", "rep": {"openState": "Close"}},
|
||||||
|
{"href": "/configuration/vs/0", "rep": {"x.com.samsung.da.countryCode": "", "x.com.samsung.da.region": ""}},
|
||||||
|
{"href": "/defrost/delay/0", "rep": {"value": false}},
|
||||||
|
{"href": "/defrost/delay/vs/0", "rep": {"x.com.samsung.da.delayDefrost": "Off"}},
|
||||||
|
{
|
||||||
|
"href": "/defrost/reservation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Summer Season",
|
||||||
|
"x.com.samsung.da.startTime": "0000-05-01T15:00:00",
|
||||||
|
"x.com.samsung.da.period": "04:00:00",
|
||||||
|
"x.com.samsung.da.endTime": "0000-10-31T00:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Winter Season",
|
||||||
|
"x.com.samsung.da.startTime": "0000-11-01T06:00:00",
|
||||||
|
"x.com.samsung.da.period": "04:00:00",
|
||||||
|
"x.com.samsung.da.endTime": "0000-04-30T00:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"href": "/icemaker/status/0", "rep": {"status": "On"}},
|
||||||
|
{"href": "/icemaker/status/vs/0", "rep": {"x.com.samsung.da.iceMaker": "On"}},
|
||||||
|
{"href": "/refrigeration/0", "rep": {"defrost": false, "rapidFreeze": false, "rapidCool": false}},
|
||||||
|
{
|
||||||
|
"href": "/refrigeration/vs/0",
|
||||||
|
"rep": {"x.com.samsung.da.rapidFridge": "Off", "x.com.samsung.da.rapidFreezing": "Off"}
|
||||||
|
},
|
||||||
|
{"href": "/drlc/0", "rep": {"DRLevel": 0, "override": false}},
|
||||||
|
{
|
||||||
|
"href": "/drlc/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.drlcLevel": "0",
|
||||||
|
"x.com.samsung.da.override": "Off",
|
||||||
|
"x.com.samsung.da.durationminutes": "0",
|
||||||
|
"x.com.samsung.da.realSaving": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"href": "/bespoke/vs/0", "rep": {"x.com.samsung.da.BespokeProduct": "On"}},
|
||||||
|
{
|
||||||
|
"href": "/otninformation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.target": "",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "false",
|
||||||
|
"otnStatus": "None",
|
||||||
|
"flashingProgress": "",
|
||||||
|
"otnList": [
|
||||||
|
{"type": "WIFI", "modelId": "A-RFWW-TP1-22-REV1", "versions": ["20250420"], "visVersion": "250420"},
|
||||||
|
{"type": "Micom", "modelId": "024100148241FFFFFFFF", "versions": ["23071923", "FFFFFFFF"], "visVersion": "230719"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/filter/waterfilter/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.filterUsage": "31",
|
||||||
|
"x.com.samsung.da.filterUsageResolution": "1",
|
||||||
|
"x.com.samsung.da.filterStatus": "normal",
|
||||||
|
"x.com.samsung.da.filterResetType": ["replaceable"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"href": "/autofill/vs/0", "rep": {"x.com.samsung.da.autofill": "Off"}},
|
||||||
|
{
|
||||||
|
"href": "/icemaker/one/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.iceMaker.name": "CUBED_ICE",
|
||||||
|
"x.com.samsung.da.iceMaker.state": "On",
|
||||||
|
"x.com.samsung.da.iceMaker.type": "toggle",
|
||||||
|
"x.com.samsung.da.iceMaker.iceMakingStatus": "ICESTATUS_STOP",
|
||||||
|
"x.com.samsung.da.iceType.desired": "NORMAL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/icemaker/two/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.iceMaker.name": "ICE_BITES",
|
||||||
|
"x.com.samsung.da.iceMaker.state": "On",
|
||||||
|
"x.com.samsung.da.iceMaker.type": "toggle",
|
||||||
|
"x.com.samsung.da.iceMaker.iceMakingStatus": "ICESTATUS_STOP",
|
||||||
|
"x.com.samsung.da.iceType.desired": "On"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"href": "/energy/ailevel/vs/0", "rep": {"aiLevel": "1", "supportedAiLevel": ["1"]}},
|
||||||
|
{
|
||||||
|
"href": "/connectionconfig/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"autoReconnectionMinVersion": "1.0",
|
||||||
|
"autoReconnection": "true",
|
||||||
|
"autoReconnectionProtocolType": ["ble_ocf", null],
|
||||||
|
"supportedWiFiAuthType": ["OPEN", "WEP", "WPA-PSK", "WPA2-PSK", "SAE"],
|
||||||
|
"supportedWiFiCryptoType": ["TKIP", "AES", "WEP-64", "WEP-128"],
|
||||||
|
"supportedWiFiFreq": ["2.4G"],
|
||||||
|
"calmConnectionCare": {"version": "1.0", "role": ["things"]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"href": "/timezone/vs/0", "rep": {"timezoneid": "America/Chicago", "offset": "-05:00", "DST": "ON"}},
|
||||||
|
{"href": "/wirelessinfo/vs/0", "rep": {"macaddressWiFi": "REDACTED", "macaddressBLE": "REDACTED"}},
|
||||||
|
{"href": "/quickcontrol/info/vs/0", "rep": {"supportedVersion": "1.0"}}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
{
|
||||||
|
"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": "/temperatures/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Freezer",
|
||||||
|
"x.com.samsung.da.desired": "0",
|
||||||
|
"x.com.samsung.da.current": "0",
|
||||||
|
"x.com.samsung.da.maximum": "5",
|
||||||
|
"x.com.samsung.da.minimum": "-8",
|
||||||
|
"x.com.samsung.da.unit": "Fahrenheit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Fridge",
|
||||||
|
"x.com.samsung.da.desired": "37",
|
||||||
|
"x.com.samsung.da.current": "37",
|
||||||
|
"x.com.samsung.da.maximum": "44",
|
||||||
|
"x.com.samsung.da.minimum": "34",
|
||||||
|
"x.com.samsung.da.unit": "Fahrenheit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperature/current/freezer/0",
|
||||||
|
"rep": {
|
||||||
|
"range": [
|
||||||
|
-8.0,
|
||||||
|
5.0
|
||||||
|
],
|
||||||
|
"units": "F",
|
||||||
|
"temperature": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperature/desired/freezer/0",
|
||||||
|
"rep": {
|
||||||
|
"range": [
|
||||||
|
-8.0,
|
||||||
|
5.0
|
||||||
|
],
|
||||||
|
"units": "F",
|
||||||
|
"temperature": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperature/current/cooler/0",
|
||||||
|
"rep": {
|
||||||
|
"range": [
|
||||||
|
34.0,
|
||||||
|
44.0
|
||||||
|
],
|
||||||
|
"units": "F",
|
||||||
|
"temperature": 37.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/temperature/desired/cooler/0",
|
||||||
|
"rep": {
|
||||||
|
"range": [
|
||||||
|
34.0,
|
||||||
|
44.0
|
||||||
|
],
|
||||||
|
"units": "F",
|
||||||
|
"temperature": 37.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/diagnosis/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/energy/consumption/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.cumulativeConsumption": "288048",
|
||||||
|
"x.com.samsung.da.cumulativePower": "1797027",
|
||||||
|
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||||
|
"x.com.samsung.da.instantaneousPower": "115",
|
||||||
|
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||||
|
"x.com.samsung.da.monthlyConsumption": "55400",
|
||||||
|
"x.com.samsung.da.thismonthlyConsumption": "39737"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"HOMECARE_WIZARD_V2",
|
||||||
|
"ENERGY_REPORT_MODEL",
|
||||||
|
"18K_REF_OUTDOOR_CONTROL_V2",
|
||||||
|
"SUPPORT_SABBATH_CONTROL"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modes": [
|
||||||
|
"CVN_CONVERTIBLE_ZONE",
|
||||||
|
"CV_FDR_BEVERAGE",
|
||||||
|
"WATERFILTER_DISABLE"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.supportedOptions": [
|
||||||
|
"CV_FDR_WINE",
|
||||||
|
"CV_FDR_DELI",
|
||||||
|
"CV_FDR_BEVERAGE",
|
||||||
|
"CV_FDR_MEAT"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/mode/0",
|
||||||
|
"rep": {
|
||||||
|
"supportedModes": [
|
||||||
|
"HOMECARE_WIZARD_V2",
|
||||||
|
"ENERGY_REPORT_MODEL",
|
||||||
|
"18K_REF_OUTDOOR_CONTROL_V2",
|
||||||
|
"SUPPORT_SABBATH_CONTROL"
|
||||||
|
],
|
||||||
|
"modes": [
|
||||||
|
"CVN_CONVERTIBLE_ZONE",
|
||||||
|
"CV_FDR_BEVERAGE",
|
||||||
|
"WATERFILTER_DISABLE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/defrost/block/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"DEFROST_BLOCK_ON",
|
||||||
|
"DEFROST_BLOCK_OFF"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.modes": [
|
||||||
|
"DEFROST_BLOCK_OFF"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/sabbath/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.sabbathMode": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/realtimenotiforclient/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.timeforshortnoti": "0",
|
||||||
|
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modelNum": "TP2X_REF_20K|00113141|0002033F051311200103000010000000",
|
||||||
|
"x.com.samsung.da.description": "TP2X_REF_20K",
|
||||||
|
"x.com.samsung.da.serialNum": "REDACTED",
|
||||||
|
"x.com.samsung.da.otnDUID": "REDACTED",
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "WiFi Module",
|
||||||
|
"x.com.samsung.da.type": "Software",
|
||||||
|
"x.com.samsung.da.number": "02144A220110",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Micom",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.number": "2011030E,FFFFFFFF",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/file/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.timeoffset": "+00:00",
|
||||||
|
"x.com.samsung.supprtedtype": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/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.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Door",
|
||||||
|
"x.com.samsung.da.openState": "Close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "2",
|
||||||
|
"x.com.samsung.da.description": "Door",
|
||||||
|
"x.com.samsung.da.openState": "Close"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/door/freezer/0",
|
||||||
|
"rep": {
|
||||||
|
"openState": "Close"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/door/cooler/0",
|
||||||
|
"rep": {
|
||||||
|
"openState": "Close"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/door/cvroom/0",
|
||||||
|
"rep": {
|
||||||
|
"openState": "Close"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/configuration/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.countryCode": "US",
|
||||||
|
"x.com.samsung.da.region": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/defrost/delay/0",
|
||||||
|
"rep": {
|
||||||
|
"value": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/defrost/delay/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.delayDefrost": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/defrost/reservation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "Summer Season",
|
||||||
|
"x.com.samsung.da.startTime": "0000-05-01T15:00:00",
|
||||||
|
"x.com.samsung.da.period": "04:00:00",
|
||||||
|
"x.com.samsung.da.endTime": "0000-10-31T00:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Winter Season",
|
||||||
|
"x.com.samsung.da.startTime": "0000-11-01T06:00:00",
|
||||||
|
"x.com.samsung.da.period": "04:00:00",
|
||||||
|
"x.com.samsung.da.endTime": "0000-04-30T00:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/icemaker/status/0",
|
||||||
|
"rep": {
|
||||||
|
"status": "On"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/icemaker/status/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.iceMaker": "On"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/refrigeration/0",
|
||||||
|
"rep": {
|
||||||
|
"defrost": false,
|
||||||
|
"rapidFreeze": false,
|
||||||
|
"rapidCool": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/refrigeration/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.rapidFridge": "Off",
|
||||||
|
"x.com.samsung.da.rapidFreezing": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/drlc/0",
|
||||||
|
"rep": {
|
||||||
|
"DRLevel": 0,
|
||||||
|
"override": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/drlc/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.drlcLevel": "0",
|
||||||
|
"x.com.samsung.da.override": "Off",
|
||||||
|
"x.com.samsung.da.durationminutes": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/otninformation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.target": "Micom",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/filter/waterfilter/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.filterUsage": "0",
|
||||||
|
"x.com.samsung.da.filterUsageResolution": "1",
|
||||||
|
"x.com.samsung.da.filterStatus": "normal",
|
||||||
|
"x.com.samsung.da.filterResetType": [
|
||||||
|
"replaceable"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/icemaker/one/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.iceMaker.name": "ICE_MAKER",
|
||||||
|
"x.com.samsung.da.iceMaker.state": "On",
|
||||||
|
"x.com.samsung.da.iceMaker.type": "toggle",
|
||||||
|
"x.com.samsung.da.iceMaker.iceMakingStatus": "ICESTATUS_STOP",
|
||||||
|
"x.com.samsung.da.iceType.desired": "NORMAL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+15
@@ -44,6 +44,21 @@
|
|||||||
"href": "/energy/consumption/0",
|
"href": "/energy/consumption/0",
|
||||||
"rep": {}
|
"rep": {}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"href": "/energy/ailevel/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"aiLevel": "0",
|
||||||
|
"supportedAiLevel": [
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
"energySavingInfo": [
|
||||||
|
{
|
||||||
|
"aiLevel": "1",
|
||||||
|
"maxEnergySavingRate": "70"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"href": "/course/vs/0",
|
"href": "/course/vs/0",
|
||||||
"rep": {
|
"rep": {
|
||||||
|
|||||||
+325
@@ -0,0 +1,325 @@
|
|||||||
|
{
|
||||||
|
"device0": [
|
||||||
|
{
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.devcol",
|
||||||
|
"oic.wk.col"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.ll",
|
||||||
|
"oic.if.b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/realtimenotiforclient/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.timeforshortnoti": "0",
|
||||||
|
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/alarms/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/diagnosis/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/energy/consumption/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.instantaneousPower": "-500",
|
||||||
|
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||||
|
"x.com.samsung.da.cumulativePower": "1210200",
|
||||||
|
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||||
|
"x.com.samsung.da.cumulativeDate": "1784671200",
|
||||||
|
"x.com.samsung.da.cumulativeDateUTC": "1784667600"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/energy/consumption/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/course/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"HOMECARE_WIZARD_V2"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.options": [
|
||||||
|
"DeviceType_0167",
|
||||||
|
"UpdateAllow_NotAllowed",
|
||||||
|
"Course_1C",
|
||||||
|
"AiOption_On",
|
||||||
|
"LaundryOutTime_0",
|
||||||
|
"AddWashSet_0",
|
||||||
|
"AddWashAvailable_7",
|
||||||
|
"AddWashIndicator_On",
|
||||||
|
"SeamlessControl_Enable",
|
||||||
|
"KidsLockBypass_On",
|
||||||
|
"WashingTimes_25",
|
||||||
|
"DrumCleanProposal_40",
|
||||||
|
"DrumCleanLog_2024-07-10T22:36:33|2024-09-12T21:37:17|2024-12-06T02:13:28|2025-03-04T20:40:47|2025-05-25T11:02:07|2025-08-17T19:48:10|2025-10-30T20:20:13|2026-01-08T21:44:02|2026-04-02T21:17:40|2026-06-08T19:51:07",
|
||||||
|
"DetergentOnce_1",
|
||||||
|
"DetergentLeft_0",
|
||||||
|
"DetergentBase_5",
|
||||||
|
"DetergentAlarm_Off",
|
||||||
|
"DetergentType_2",
|
||||||
|
"DetergentTotal_49",
|
||||||
|
"SoftenerOnce_0",
|
||||||
|
"SoftenerLeft_0",
|
||||||
|
"SoftenerBase_0",
|
||||||
|
"SoftenerAlarm_Off",
|
||||||
|
"SoftenerType_0",
|
||||||
|
"SoftenerTotal_0",
|
||||||
|
"SpecialFunction_4",
|
||||||
|
"AvailableDelayTime_159",
|
||||||
|
"BubbleSoak_Off",
|
||||||
|
"LaundryPlannerUserSetTime_0",
|
||||||
|
"ProgressTimeSet_420BF48204D4A204F5",
|
||||||
|
"SendToDevice_Off",
|
||||||
|
"GMT_02",
|
||||||
|
"LaundryPlannerMicomSetTime_1704",
|
||||||
|
"PreWashSetting_Off",
|
||||||
|
"IntensiveSetting_Off",
|
||||||
|
"BubbleSoakSet_00F000F000F000F0F0F0F00000F000F0F00000F000000000",
|
||||||
|
"EnergyLevelSet_05030401040104020404020503020202030301010505050405",
|
||||||
|
"MostUsed_1C8410923FA67FB01F0000000000",
|
||||||
|
"PreWashAvailableSet_F0F000F0F0F000F0F0F0F00000F0F0F0F00000F000000000",
|
||||||
|
"IntensiveAvailableSet_F0F000F0F0F000F0F0F0F00000F0F0F0F00000F000000000",
|
||||||
|
"GeoFenceAlarm",
|
||||||
|
"UsagesDB_ok",
|
||||||
|
"EnergyKW_396",
|
||||||
|
"DrumCleanLog_2024-07-10T22:36:33|2024-09-12T21:37:17|2024-12-06T02:13:28|2025-03-04T20:40:47|2025-05-25T11:02:07|2025-08-17T19:48:10|2025-10-30T20:20:13|2026-01-08T21:44:02|2026-04-02T21:17:40|2026-06-08T19:51:07",
|
||||||
|
"TimeSync_NotSupported"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.supportedOptions": [
|
||||||
|
"41C8410923FA67FB01F1B847E923FA67FB01F1E811E933FA53FB01F1D841E923FA67FB01F1F8102923FA57FB01F25843E923FA57FB01F26841E920FA207B01F33857E933FA67FB00024841E930FA30FB00032833E923FA37FB00020857E943FA67FB01F22841E920FA30FB00023831E930FA57FB01F2F831E933FA30FB00021831E943FA57FB0002D841E923FA30FB00030843E923FA67FB000278000913FA67FB01F2880009000A67EB01F368410923FA640B11E3880009000A000B11E3980009000A000B11E3780009000A000B0002985209204A520B000"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/power/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.power": "Off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/power/0",
|
||||||
|
"rep": {
|
||||||
|
"value": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/cycleinterface/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/kidslock/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.kidsLock": "Ready"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/kidslock/0",
|
||||||
|
"rep": {
|
||||||
|
"value": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/operational/state/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.state": "Ready",
|
||||||
|
"x.com.samsung.da.remainingTime": "02:39:00",
|
||||||
|
"x.com.samsung.da.progressPercentage": "1",
|
||||||
|
"x.com.samsung.da.progress": "None",
|
||||||
|
"x.com.samsung.da.delayEndTime": "00:00:00",
|
||||||
|
"x.com.samsung.da.supportedProgress": [
|
||||||
|
"None",
|
||||||
|
"Weightsensing",
|
||||||
|
"Wash",
|
||||||
|
"Rinse",
|
||||||
|
"Spin",
|
||||||
|
"Finish"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/operational/state/0",
|
||||||
|
"rep": {
|
||||||
|
"currentMachineState": "**REDACTED**",
|
||||||
|
"machineStates": "**REDACTED**",
|
||||||
|
"jobStates": [
|
||||||
|
"None",
|
||||||
|
"Weightsensing",
|
||||||
|
"Wash",
|
||||||
|
"Rinse",
|
||||||
|
"Spin",
|
||||||
|
"Finish"
|
||||||
|
],
|
||||||
|
"currentJobState": "None",
|
||||||
|
"remainingTime": "02:39:00",
|
||||||
|
"progressPercentage": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modelNum": "DA_WM_TP2_20_COMMON|20260041|20010102011411B402A3020700000000",
|
||||||
|
"x.com.samsung.da.description": "DA_WM_TP2_20_COMMON_WW6600R/DC92-02617A_5049",
|
||||||
|
"x.com.samsung.da.serialNum": "REDACTED",
|
||||||
|
"x.com.samsung.da.otnDUID": "REDACTED",
|
||||||
|
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
|
||||||
|
"x.com.samsung.da.diagLogType": [
|
||||||
|
"errCode",
|
||||||
|
"dump"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.diagDumpType": "file",
|
||||||
|
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||||
|
"x.com.samsung.da.diagMnid": "0AJT",
|
||||||
|
"x.com.samsung.da.diagSetupid": "210",
|
||||||
|
"x.com.samsung.da.diagMinVersion": "1.0",
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "DA_WM_TP2_20_COMMON|20260041|20010102011411B402A3020700000000",
|
||||||
|
"x.com.samsung.da.type": "Software",
|
||||||
|
"x.com.samsung.da.number": "02674A250414(F546)",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "Firmware_1_DB_20260041200911390FFFFF202617412101203104FFFF(01672026004120261741_30000000)(FileDown:0)(Type:0)",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.number": "20091139,21012031",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "2",
|
||||||
|
"x.com.samsung.da.description": "Firmware_2_DB_2025984120072205042FFFFFFFFFFFFFFFFFFFFFFFFE(016720259841FFFFFFFF_30000000)(FileDown:0)(Type:0)",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.number": "20072205,FFFFFFFF"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/file/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.timeoffset": "+01:00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/washer/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.waterTemperature": "40",
|
||||||
|
"x.com.samsung.da.supportedWaterTemperature": [
|
||||||
|
"None",
|
||||||
|
"Cold",
|
||||||
|
"20",
|
||||||
|
"30",
|
||||||
|
"40",
|
||||||
|
"60",
|
||||||
|
"90"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.spinLevel": "1400",
|
||||||
|
"x.com.samsung.da.supportedSpinLevel": [
|
||||||
|
"RinseHold",
|
||||||
|
"NoSpin",
|
||||||
|
"400",
|
||||||
|
"800",
|
||||||
|
"1000",
|
||||||
|
"1200",
|
||||||
|
"1400"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.dryLevel": "None",
|
||||||
|
"x.com.samsung.da.supportedDryLevel": [
|
||||||
|
"None",
|
||||||
|
"Cupboard",
|
||||||
|
"30",
|
||||||
|
"60",
|
||||||
|
"90",
|
||||||
|
"120",
|
||||||
|
"180",
|
||||||
|
"240"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.rinseCycles": "2",
|
||||||
|
"x.com.samsung.da.supportedRinseCycles": [
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/st/washercourse/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.st.washerMode": "Table_02_Course_1C",
|
||||||
|
"x.com.samsung.da.st.courseTable": "Table_02"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/setting/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.supportedSetLanguage": [
|
||||||
|
"ko_KR",
|
||||||
|
"en_US"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.setLanguage": "en_GB"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wm/editcourse/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.editCourseList": "EditCourseList_1C3026241E293633281D27211B202F2223323825392D1F37",
|
||||||
|
"x.com.samsung.da.fixedCourseList": "FixedCourseList_1C353A29"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wm/setinfo/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.isModelSettingWithoutSC": "true",
|
||||||
|
"x.com.samsung.da.aiCourse": "true_formobile",
|
||||||
|
"x.com.samsung.da.isModelSettingPowerOnOff": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wm/jobbeginingstatus/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.currentStatus": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/otninformation/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.target": "Micom",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/remotectrl/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.remoteControlEnabled": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/remotectrl/0",
|
||||||
|
"rep": {
|
||||||
|
"value": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/configuration/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.region": "0000000000",
|
||||||
|
"x.com.samsung.da.countryCode": "GB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+253
@@ -0,0 +1,253 @@
|
|||||||
|
{
|
||||||
|
"device0": [
|
||||||
|
{
|
||||||
|
"rt": [
|
||||||
|
"x.com.samsung.devcol",
|
||||||
|
"oic.wk.col"
|
||||||
|
],
|
||||||
|
"if": [
|
||||||
|
"oic.if.baseline",
|
||||||
|
"oic.if.ll",
|
||||||
|
"oic.if.b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/realtimenotiforclient/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.timeforshortnoti": "0",
|
||||||
|
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/alarms/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/diagnosis/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/energy/consumption/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||||
|
"x.com.samsung.da.instantaneousPower": "-500",
|
||||||
|
"x.com.samsung.da.cumulativePower": "199200",
|
||||||
|
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||||
|
"x.com.samsung.da.cumulativeDate": "1784631600",
|
||||||
|
"x.com.samsung.da.cumulativeDateUTC": "1784646000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/energy/consumption/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/course/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.supportedModes": [
|
||||||
|
"HOMECARE_WIZARD_V2"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.options": [
|
||||||
|
"DeviceType_0146",
|
||||||
|
"UpdateAllow_NotAllowed",
|
||||||
|
"Course_A5",
|
||||||
|
"LaundryOutTime_0",
|
||||||
|
"AddGarmentIndicator_Off",
|
||||||
|
"SteamWash_Enable",
|
||||||
|
"SeamlessControl_Disable",
|
||||||
|
"KidsLockBypass_On",
|
||||||
|
"WashingTimes_14",
|
||||||
|
"DrumCleanProposal_40",
|
||||||
|
"DetergentOnce_0",
|
||||||
|
"DetergentLeft_0",
|
||||||
|
"DetergentBase_0",
|
||||||
|
"DetergentAlarm_Off",
|
||||||
|
"DetergentType_0",
|
||||||
|
"DetergentTotal_0",
|
||||||
|
"SpecialFunction_4",
|
||||||
|
"AvailableDelayTime_124",
|
||||||
|
"LaundryPlannerUserSetTime_0",
|
||||||
|
"ProgressTimeSet_4210E08209F6A2021C",
|
||||||
|
"EnergyLevelSet_0502040502030302030105",
|
||||||
|
"UsagesDB_ok",
|
||||||
|
"EnergyKW_396",
|
||||||
|
"DrumCleanLog_2023-08-11T19:17:11|2023-11-02T03:23:13|2023-12-22T22:35:54|2024-04-08T01:27:32|2024-04-22T01:07:26|2024-10-02T19:16:45|2025-02-03T02:01:33|2025-06-16T17:36:13|2026-01-04T18:34:46|2026-05-26T02:33:16",
|
||||||
|
"TimeSync_NotSupported"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.supportedOptions": [
|
||||||
|
"301833EA41BC20E9C843EA41FC30EA98520A41FC20EA2810EA207C20EAA831EA41FC20EA5830EA20FC20E9F831EA41FC10E9E830EA21FC20E9D8000A41FC000B18520A410C000"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/power/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.power": "On"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/power/0",
|
||||||
|
"rep": {
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/cycleinterface/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/kidslock/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.kidsLock": "Ready"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/kidslock/0",
|
||||||
|
"rep": {
|
||||||
|
"value": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/operational/state/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.state": "Ready",
|
||||||
|
"x.com.samsung.da.remainingTime": "02:04: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",
|
||||||
|
"Wash",
|
||||||
|
"Rinse",
|
||||||
|
"Spin",
|
||||||
|
"Finish"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/operational/state/0",
|
||||||
|
"rep": {
|
||||||
|
"currentMachineState": "**REDACTED**",
|
||||||
|
"machineStates": "**REDACTED**",
|
||||||
|
"jobStates": [
|
||||||
|
"None",
|
||||||
|
"Wash",
|
||||||
|
"Rinse",
|
||||||
|
"Spin",
|
||||||
|
"Finish"
|
||||||
|
],
|
||||||
|
"currentJobState": "None",
|
||||||
|
"remainingTime": "02:04:00",
|
||||||
|
"progressPercentage": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.modelNum": "DA_WM_A51_20_COMMON|20198042|20020001001111400203000000000000",
|
||||||
|
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON_WV9600M/DC92-01980B_0014",
|
||||||
|
"x.com.samsung.da.serialNum": "REDACTED",
|
||||||
|
"x.com.samsung.da.otnDUID": "REDACTED",
|
||||||
|
"x.com.samsung.da.items": [
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "0",
|
||||||
|
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON|20198042|20020001001111400203000000000000",
|
||||||
|
"x.com.samsung.da.type": "Software",
|
||||||
|
"x.com.samsung.da.number": "02198A230708(E257)",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x.com.samsung.da.id": "1",
|
||||||
|
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON",
|
||||||
|
"x.com.samsung.da.type": "Firmware",
|
||||||
|
"x.com.samsung.da.number": "18030902,18090603",
|
||||||
|
"x.com.samsung.da.newVersionAvailable": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/file/information/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.timeoffset": "-04:00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/washer/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.waterTemperature": "Warm",
|
||||||
|
"x.com.samsung.da.supportedWaterTemperature": [
|
||||||
|
"None",
|
||||||
|
"Cold",
|
||||||
|
"Cool",
|
||||||
|
"Warm",
|
||||||
|
"Hot",
|
||||||
|
"ExtraHot"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.spinLevel": "Low",
|
||||||
|
"x.com.samsung.da.supportedSpinLevel": [
|
||||||
|
"RinseHold",
|
||||||
|
"NoSpin",
|
||||||
|
"Low",
|
||||||
|
"Medium",
|
||||||
|
"High"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.soilLevel": "Heavy",
|
||||||
|
"x.com.samsung.da.supportedSoilLevel": [
|
||||||
|
"None",
|
||||||
|
"Light",
|
||||||
|
"Normal",
|
||||||
|
"Heavy"
|
||||||
|
],
|
||||||
|
"x.com.samsung.da.rinseCycles": "3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/st/washercourse/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.st.washerMode": "Table_00_Course_A5",
|
||||||
|
"x.com.samsung.da.st.courseTable": "Table_00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/setting/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wm/editcourse/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wm/setinfo/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.isModelSettingWithoutSC": "false",
|
||||||
|
"x.com.samsung.da.isModelSettingPowerOnOff": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/wm/jobbeginingstatus/vs/0",
|
||||||
|
"rep": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/remotectrl/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.remoteControlEnabled": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/remotectrl/0",
|
||||||
|
"rep": {
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": "/configuration/vs/0",
|
||||||
|
"rep": {
|
||||||
|
"x.com.samsung.da.region": "0000000000",
|
||||||
|
"x.com.samsung.da.countryCode": "US"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -3,8 +3,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import cbor2
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||||
|
|
||||||
@@ -150,12 +152,26 @@ class FakeObserveSession:
|
|||||||
self.subscribed: list[str] = []
|
self.subscribed: list[str] = []
|
||||||
self.fail_hrefs: set[str] = set()
|
self.fail_hrefs: set[str] = set()
|
||||||
self.closed = False
|
self.closed = False
|
||||||
|
# When set to a rep dict, subscribe() immediately delivers that rep
|
||||||
|
# as an OBSERVE notification for the href — what a real device does
|
||||||
|
# when it answers a subscription with the current representation.
|
||||||
|
# `try_enter_observe_mode` clears its notified set before it
|
||||||
|
# subscribes, so this is the only way to deliver a notify that
|
||||||
|
# reliably counts: it lands synchronously via on_notification,
|
||||||
|
# after that clear and before the post-sleep fraction check — a
|
||||||
|
# notify raced in from another thread can be wiped by the clear
|
||||||
|
# (or arrive after the check) depending on scheduling. Set to
|
||||||
|
# None to model a device that answers subscriptions but never
|
||||||
|
# notifies.
|
||||||
|
self.notify_on_subscribe: dict[str, Any] | None = None
|
||||||
|
|
||||||
def subscribe(self, path_segs):
|
def subscribe(self, path_segs):
|
||||||
href = '/' + '/'.join(path_segs)
|
href = '/' + '/'.join(path_segs)
|
||||||
if href in self.fail_hrefs:
|
if href in self.fail_hrefs:
|
||||||
raise ConnectionError("subscribe failed")
|
raise ConnectionError("subscribe failed")
|
||||||
self.subscribed.append(href)
|
self.subscribed.append(href)
|
||||||
|
if self.notify_on_subscribe is not None and self.on_notification is not None:
|
||||||
|
self.on_notification(href, cbor2.dumps(self.notify_on_subscribe))
|
||||||
return b'\x01'
|
return b'\x01'
|
||||||
|
|
||||||
def refresh_observes(self, paths):
|
def refresh_observes(self, paths):
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
"""Tests for the LocalThingsCoordinator."""
|
"""Tests for the LocalThingsCoordinator."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import cbor2
|
import cbor2
|
||||||
import pytest
|
import pytest
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers import issue_registry as ir
|
from homeassistant.helpers import issue_registry as ir
|
||||||
|
|
||||||
from custom_components.localthings.const import (
|
from custom_components.localthings.const import (
|
||||||
CONF_HOST, DOMAIN, SUMMARY_INTERVAL_S,
|
CONF_HOST, DOMAIN, SUMMARY_INTERVAL_S,
|
||||||
)
|
)
|
||||||
from custom_components.localthings.coordinator import LocalThingsCoordinator
|
from custom_components.localthings.coordinator import LocalThingsCoordinator
|
||||||
|
from custom_components.localthings.registry.capabilities.common import (
|
||||||
|
remote_control_enabled,
|
||||||
|
)
|
||||||
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
|
||||||
|
|
||||||
from .conftest import ENTRY_DATA, MOCK_SERIAL
|
from .conftest import ENTRY_DATA, MOCK_SERIAL
|
||||||
@@ -260,21 +262,18 @@ async def test_enters_observe_mode_when_hot_warm_hrefs_notify(
|
|||||||
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
||||||
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
||||||
|
|
||||||
# try_enter_observe_mode clears prior notifications as soon as it
|
# try_enter_observe_mode clears prior notifications before it
|
||||||
# starts, so notifies must land *during* its grace-period sleep (same
|
# subscribes, then checks the notified fraction after the grace-period
|
||||||
# pattern test_observe.py uses), not before the call.
|
# sleep — a notify counts as long as it arrives in that window, not
|
||||||
def _notify_during_grace_period():
|
# specifically during the sleep itself. The fake session delivers one
|
||||||
time.sleep(0.005)
|
# synchronously from subscribe() for every href it subscribes to (see
|
||||||
for href in hrefs:
|
# FakeObserveSession.notify_on_subscribe in conftest.py), which lands
|
||||||
fake.on_notification(href, cbor2.dumps({'notified': True}))
|
# it in that window by construction rather than by timing.
|
||||||
|
fake.notify_on_subscribe = {'notified': True}
|
||||||
notifier = threading.Thread(target=_notify_during_grace_period, daemon=True)
|
|
||||||
notifier.start()
|
|
||||||
entered = await hass.async_add_executor_job(
|
entered = await hass.async_add_executor_job(
|
||||||
coordinator._observe.try_enter_observe_mode,
|
coordinator._observe.try_enter_observe_mode,
|
||||||
fake, hrefs, 0.02, 0.8,
|
fake, hrefs, 0.02, 0.8,
|
||||||
)
|
)
|
||||||
notifier.join()
|
|
||||||
|
|
||||||
assert entered is True
|
assert entered is True
|
||||||
assert coordinator._observe.mode == MODE_OBSERVE
|
assert coordinator._observe.mode == MODE_OBSERVE
|
||||||
@@ -300,18 +299,11 @@ async def test_reconnect_while_observe_mode_downgrades_to_poll(
|
|||||||
|
|
||||||
# Get the coordinator into observe mode the same way
|
# Get the coordinator into observe mode the same way
|
||||||
# test_enters_observe_mode_when_hot_warm_hrefs_notify does.
|
# test_enters_observe_mode_when_hot_warm_hrefs_notify does.
|
||||||
def _notify_during_grace_period():
|
fake.notify_on_subscribe = {'notified': True}
|
||||||
time.sleep(0.005)
|
|
||||||
for href in hrefs:
|
|
||||||
fake.on_notification(href, cbor2.dumps({'notified': True}))
|
|
||||||
|
|
||||||
notifier = threading.Thread(target=_notify_during_grace_period, daemon=True)
|
|
||||||
notifier.start()
|
|
||||||
entered = await hass.async_add_executor_job(
|
entered = await hass.async_add_executor_job(
|
||||||
coordinator._observe.try_enter_observe_mode,
|
coordinator._observe.try_enter_observe_mode,
|
||||||
fake, hrefs, 0.02, 0.8,
|
fake, hrefs, 0.02, 0.8,
|
||||||
)
|
)
|
||||||
notifier.join()
|
|
||||||
assert entered is True
|
assert entered is True
|
||||||
assert coordinator.observe_mode == MODE_OBSERVE
|
assert coordinator.observe_mode == MODE_OBSERVE
|
||||||
|
|
||||||
@@ -324,12 +316,12 @@ async def test_reconnect_while_observe_mode_downgrades_to_poll(
|
|||||||
|
|
||||||
# Simulate the existing "poll failed, reconnecting" branch: _poll_once
|
# Simulate the existing "poll failed, reconnecting" branch: _poll_once
|
||||||
# fails once (triggering the reconnect/backoff path), then succeeds.
|
# fails once (triggering the reconnect/backoff path), then succeeds.
|
||||||
# No fresh notifies are supplied for the immediate resubscribe attempt
|
# The device stops notifying on subscribe, so the immediate resubscribe
|
||||||
# this now triggers, so its grace period (shortened for tests by the
|
# attempt this now triggers gets nothing back and mode stays 'poll' —
|
||||||
# `_fast_coordinator_timers` autouse fixture — see conftest.py) times
|
# this test only asserts the tear-down half of the fix;
|
||||||
# out and mode stays 'poll' — this test only asserts the tear-down half of
|
# test_reconnect_from_observe_mode_resubscribes_immediately covers the
|
||||||
# the fix; test_reconnect_from_observe_mode_resubscribes_immediately
|
# successful-immediate-resubscribe half.
|
||||||
# covers the successful-immediate-resubscribe half.
|
fake.notify_on_subscribe = None
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
'custom_components.localthings.coordinator.LocalThingsCoordinator._poll_once',
|
'custom_components.localthings.coordinator.LocalThingsCoordinator._poll_once',
|
||||||
@@ -362,18 +354,11 @@ async def test_poll_timeout_skips_reconnect_when_push_is_healthy(
|
|||||||
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
||||||
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
||||||
|
|
||||||
def _notify_during_grace_period():
|
fake.notify_on_subscribe = {'notified': True}
|
||||||
time.sleep(0.005)
|
|
||||||
for href in hrefs:
|
|
||||||
fake.on_notification(href, cbor2.dumps({'notified': True}))
|
|
||||||
|
|
||||||
notifier = threading.Thread(target=_notify_during_grace_period, daemon=True)
|
|
||||||
notifier.start()
|
|
||||||
entered = await hass.async_add_executor_job(
|
entered = await hass.async_add_executor_job(
|
||||||
coordinator._observe.try_enter_observe_mode,
|
coordinator._observe.try_enter_observe_mode,
|
||||||
fake, hrefs, 0.02, 0.8,
|
fake, hrefs, 0.02, 0.8,
|
||||||
)
|
)
|
||||||
notifier.join()
|
|
||||||
assert entered is True
|
assert entered is True
|
||||||
assert coordinator.observe_mode == MODE_OBSERVE
|
assert coordinator.observe_mode == MODE_OBSERVE
|
||||||
|
|
||||||
@@ -408,18 +393,11 @@ async def test_poll_timeout_reconnects_after_consecutive_limit_even_with_push(
|
|||||||
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
||||||
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
||||||
|
|
||||||
def _notify_during_grace_period():
|
fake.notify_on_subscribe = {'notified': True}
|
||||||
time.sleep(0.005)
|
|
||||||
for href in hrefs:
|
|
||||||
fake.on_notification(href, cbor2.dumps({'notified': True}))
|
|
||||||
|
|
||||||
notifier = threading.Thread(target=_notify_during_grace_period, daemon=True)
|
|
||||||
notifier.start()
|
|
||||||
entered = await hass.async_add_executor_job(
|
entered = await hass.async_add_executor_job(
|
||||||
coordinator._observe.try_enter_observe_mode,
|
coordinator._observe.try_enter_observe_mode,
|
||||||
fake, hrefs, 0.02, 0.8,
|
fake, hrefs, 0.02, 0.8,
|
||||||
)
|
)
|
||||||
notifier.join()
|
|
||||||
assert entered is True
|
assert entered is True
|
||||||
assert coordinator.observe_mode == MODE_OBSERVE
|
assert coordinator.observe_mode == MODE_OBSERVE
|
||||||
|
|
||||||
@@ -441,7 +419,10 @@ async def test_poll_timeout_reconnects_after_consecutive_limit_even_with_push(
|
|||||||
assert coordinator.observe_mode == MODE_OBSERVE
|
assert coordinator.observe_mode == MODE_OBSERVE
|
||||||
|
|
||||||
# The final consecutive timeout crosses the limit and triggers the
|
# The final consecutive timeout crosses the limit and triggers the
|
||||||
# existing reconnect path, which then succeeds and downgrades.
|
# existing reconnect path, which then succeeds and downgrades. The
|
||||||
|
# device stops notifying on subscribe, so the resubscribe attempt that
|
||||||
|
# follows the reconnect finds nothing and mode stays 'poll'.
|
||||||
|
fake.notify_on_subscribe = None
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
'custom_components.localthings.coordinator.LocalThingsCoordinator._poll_once',
|
'custom_components.localthings.coordinator.LocalThingsCoordinator._poll_once',
|
||||||
@@ -473,18 +454,11 @@ async def test_poll_timeout_counter_resets_when_push_is_healthy_again(
|
|||||||
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
||||||
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
||||||
|
|
||||||
def _notify_during_grace_period():
|
fake.notify_on_subscribe = {'notified': True}
|
||||||
time.sleep(0.005)
|
|
||||||
for href in hrefs:
|
|
||||||
fake.on_notification(href, cbor2.dumps({'notified': True}))
|
|
||||||
|
|
||||||
notifier = threading.Thread(target=_notify_during_grace_period, daemon=True)
|
|
||||||
notifier.start()
|
|
||||||
entered = await hass.async_add_executor_job(
|
entered = await hass.async_add_executor_job(
|
||||||
coordinator._observe.try_enter_observe_mode,
|
coordinator._observe.try_enter_observe_mode,
|
||||||
fake, hrefs, 0.02, 0.8,
|
fake, hrefs, 0.02, 0.8,
|
||||||
)
|
)
|
||||||
notifier.join()
|
|
||||||
assert entered is True
|
assert entered is True
|
||||||
assert coordinator.observe_mode == MODE_OBSERVE
|
assert coordinator.observe_mode == MODE_OBSERVE
|
||||||
|
|
||||||
@@ -526,21 +500,11 @@ async def test_reconnect_from_observe_mode_resubscribes_immediately(
|
|||||||
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
||||||
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
||||||
|
|
||||||
def _notify(delay: float = 0.005) -> threading.Thread:
|
fake.notify_on_subscribe = {'notified': True}
|
||||||
def _run():
|
|
||||||
time.sleep(delay)
|
|
||||||
for href in hrefs:
|
|
||||||
fake.on_notification(href, cbor2.dumps({'notified': True}))
|
|
||||||
t = threading.Thread(target=_run, daemon=True)
|
|
||||||
t.start()
|
|
||||||
return t
|
|
||||||
|
|
||||||
notifier = _notify()
|
|
||||||
entered = await hass.async_add_executor_job(
|
entered = await hass.async_add_executor_job(
|
||||||
coordinator._observe.try_enter_observe_mode,
|
coordinator._observe.try_enter_observe_mode,
|
||||||
fake, hrefs, 0.02, 0.8,
|
fake, hrefs, 0.02, 0.8,
|
||||||
)
|
)
|
||||||
notifier.join()
|
|
||||||
assert entered is True
|
assert entered is True
|
||||||
assert coordinator.observe_mode == MODE_OBSERVE
|
assert coordinator.observe_mode == MODE_OBSERVE
|
||||||
|
|
||||||
@@ -563,10 +527,8 @@ async def test_reconnect_from_observe_mode_resubscribes_immediately(
|
|||||||
new=AsyncMock(),
|
new=AsyncMock(),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
notifier = _notify()
|
|
||||||
await coordinator.async_request_refresh()
|
await coordinator.async_request_refresh()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
notifier.join()
|
|
||||||
|
|
||||||
assert coordinator.observe_mode == MODE_OBSERVE
|
assert coordinator.observe_mode == MODE_OBSERVE
|
||||||
|
|
||||||
@@ -587,21 +549,11 @@ async def test_sweep_mismatch_never_downgrades_a_live_observe_session(
|
|||||||
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
||||||
assert len(hrefs) >= 2
|
assert len(hrefs) >= 2
|
||||||
|
|
||||||
def _notify(delay: float = 0.005) -> threading.Thread:
|
fake.notify_on_subscribe = {'notified': True}
|
||||||
def _run():
|
|
||||||
time.sleep(delay)
|
|
||||||
for href in hrefs:
|
|
||||||
fake.on_notification(href, cbor2.dumps({'notified': True}))
|
|
||||||
t = threading.Thread(target=_run, daemon=True)
|
|
||||||
t.start()
|
|
||||||
return t
|
|
||||||
|
|
||||||
notifier = _notify()
|
|
||||||
entered = await hass.async_add_executor_job(
|
entered = await hass.async_add_executor_job(
|
||||||
coordinator._observe.try_enter_observe_mode,
|
coordinator._observe.try_enter_observe_mode,
|
||||||
fake, hrefs, 0.02, 0.8,
|
fake, hrefs, 0.02, 0.8,
|
||||||
)
|
)
|
||||||
notifier.join()
|
|
||||||
assert entered is True
|
assert entered is True
|
||||||
assert coordinator.observe_mode == MODE_OBSERVE
|
assert coordinator.observe_mode == MODE_OBSERVE
|
||||||
|
|
||||||
@@ -637,21 +589,11 @@ async def test_sweep_mismatch_forces_subpolls_on_a_live_observe_session(
|
|||||||
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
||||||
assert len(hrefs) >= 2
|
assert len(hrefs) >= 2
|
||||||
|
|
||||||
def _notify(delay: float = 0.005) -> threading.Thread:
|
fake.notify_on_subscribe = {'notified': True}
|
||||||
def _run():
|
|
||||||
time.sleep(delay)
|
|
||||||
for href in hrefs:
|
|
||||||
fake.on_notification(href, cbor2.dumps({'notified': True}))
|
|
||||||
t = threading.Thread(target=_run, daemon=True)
|
|
||||||
t.start()
|
|
||||||
return t
|
|
||||||
|
|
||||||
notifier = _notify()
|
|
||||||
entered = await hass.async_add_executor_job(
|
entered = await hass.async_add_executor_job(
|
||||||
coordinator._observe.try_enter_observe_mode,
|
coordinator._observe.try_enter_observe_mode,
|
||||||
fake, hrefs, 0.02, 0.8,
|
fake, hrefs, 0.02, 0.8,
|
||||||
)
|
)
|
||||||
notifier.join()
|
|
||||||
assert entered is True
|
assert entered is True
|
||||||
assert coordinator.observe_mode == MODE_OBSERVE
|
assert coordinator.observe_mode == MODE_OBSERVE
|
||||||
|
|
||||||
@@ -696,3 +638,175 @@ async def test_write_marks_href_pending_before_post(
|
|||||||
await coordinator.async_send_command(bound, 5)
|
await coordinator.async_send_command(bound, 5)
|
||||||
|
|
||||||
assert coordinator._observe._settle_until.get('/test/vs/0') is not None
|
assert coordinator._observe._settle_until.get('/test/vs/0') is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_send_command_applies_write_optimistically_before_settling(
|
||||||
|
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session
|
||||||
|
) -> None:
|
||||||
|
"""The cache must reflect a write immediately, and stay put against a
|
||||||
|
stale echo for the rest of the settle window -- otherwise
|
||||||
|
mark_write_pending has nothing to protect and just delays the real
|
||||||
|
device confirmation instead, which reads to a user as the write being
|
||||||
|
silently reverted (issue #27)."""
|
||||||
|
from custom_components.localthings.registry.discovery import BoundEntity
|
||||||
|
from custom_components.localthings.registry.entities import NumberDesc
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
def _write_fn(payload, rep, href):
|
||||||
|
return (['some', 'path'], {'value': payload})
|
||||||
|
|
||||||
|
desc = NumberDesc(key='test', field='value', write_fn=_write_fn)
|
||||||
|
bound = BoundEntity(href='/test/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, 5)
|
||||||
|
|
||||||
|
# The optimistic value is visible right away, without waiting for a
|
||||||
|
# device response.
|
||||||
|
assert coordinator._cache.get('/test/vs/0') == {'value': 5}
|
||||||
|
|
||||||
|
# A stale update racing in behind the write (e.g. a poll/notify that
|
||||||
|
# was already in flight before the PUT) must not clobber it while the
|
||||||
|
# settle window is open.
|
||||||
|
applied = coordinator._observe.apply('/test/vs/0', {'value': 0}, source='observe')
|
||||||
|
assert applied is False
|
||||||
|
assert coordinator._cache.get('/test/vs/0') == {'value': 5}
|
||||||
|
|
||||||
|
|
||||||
|
class TestRemoteControlEnabled:
|
||||||
|
"""remote_control_enabled (registry/capabilities/common.py) is the
|
||||||
|
single source of truth for the /remotectrl on/off signal, shared by
|
||||||
|
the Smart Control binary_sensor descriptors and the coordinator's
|
||||||
|
write guard alike."""
|
||||||
|
|
||||||
|
def test_vs_fallback_href_true_is_enabled(self):
|
||||||
|
resources = {'/remotectrl/vs/0': {'x.com.samsung.da.remoteControlEnabled': 'true'}}
|
||||||
|
assert remote_control_enabled(resources) is True
|
||||||
|
|
||||||
|
def test_vs_fallback_href_false_is_disabled(self):
|
||||||
|
resources = {'/remotectrl/vs/0': {'x.com.samsung.da.remoteControlEnabled': 'false'}}
|
||||||
|
assert remote_control_enabled(resources) is False
|
||||||
|
|
||||||
|
def test_generic_href_true_is_enabled(self):
|
||||||
|
resources = {'/remotectrl/0': {'value': True}}
|
||||||
|
assert remote_control_enabled(resources) is True
|
||||||
|
|
||||||
|
def test_generic_href_false_is_disabled(self):
|
||||||
|
resources = {'/remotectrl/0': {'value': False}}
|
||||||
|
assert remote_control_enabled(resources) is False
|
||||||
|
|
||||||
|
def test_generic_href_wins_when_both_present(self):
|
||||||
|
resources = {
|
||||||
|
'/remotectrl/0': {'value': True},
|
||||||
|
'/remotectrl/vs/0': {'x.com.samsung.da.remoteControlEnabled': 'false'},
|
||||||
|
}
|
||||||
|
assert remote_control_enabled(resources) is True
|
||||||
|
|
||||||
|
def test_assumes_enabled_when_capability_absent(self):
|
||||||
|
assert remote_control_enabled({}) is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_send_command_blocked_when_remote_control_disabled(
|
||||||
|
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session
|
||||||
|
) -> None:
|
||||||
|
"""A device reporting remote control off must reject every write with
|
||||||
|
a user-facing message, ahead of write_fn's silent-no-op path and any
|
||||||
|
description-level validate_fn."""
|
||||||
|
from custom_components.localthings.registry.discovery import BoundEntity
|
||||||
|
from custom_components.localthings.registry.entities import NumberDesc
|
||||||
|
|
||||||
|
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',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _write_fn(payload, rep, href):
|
||||||
|
return (['some', 'path'], {'value': payload})
|
||||||
|
|
||||||
|
desc = NumberDesc(key='test', field='value', write_fn=_write_fn)
|
||||||
|
bound = BoundEntity(href='/test/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, match="Remote control is turned off"):
|
||||||
|
await coordinator.async_send_command(bound, 5)
|
||||||
|
|
||||||
|
assert posted is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_send_command_allowed_when_remote_control_enabled(
|
||||||
|
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session
|
||||||
|
) -> None:
|
||||||
|
"""The same write goes through once remote control reports enabled."""
|
||||||
|
from custom_components.localthings.registry.discovery import BoundEntity
|
||||||
|
from custom_components.localthings.registry.entities import NumberDesc
|
||||||
|
|
||||||
|
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': 'true'},
|
||||||
|
source='test',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _write_fn(payload, rep, href):
|
||||||
|
return (['some', 'path'], {'value': payload})
|
||||||
|
|
||||||
|
desc = NumberDesc(key='test', field='value', write_fn=_write_fn)
|
||||||
|
bound = BoundEntity(href='/test/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, 5)
|
||||||
|
|
||||||
|
assert coordinator._observe._settle_until.get('/test/vs/0') is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_send_command_remote_control_check_precedes_validate_fn(
|
||||||
|
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session
|
||||||
|
) -> None:
|
||||||
|
"""When both a disabled remote-control state and a failing validate_fn
|
||||||
|
apply to the same write, the remote-control message wins -- it's the
|
||||||
|
more actionable of the two and is checked first."""
|
||||||
|
from custom_components.localthings.registry.discovery import BoundEntity
|
||||||
|
from custom_components.localthings.registry.entities import SwitchDesc
|
||||||
|
|
||||||
|
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',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _write_fn(payload, rep, href=None):
|
||||||
|
return (['some', 'path'], {'value': payload})
|
||||||
|
|
||||||
|
def _validate_fn(payload, rep, resources):
|
||||||
|
return "always rejected"
|
||||||
|
|
||||||
|
desc = SwitchDesc(key='test', field='value', write_fn=_write_fn, validate_fn=_validate_fn)
|
||||||
|
bound = BoundEntity(href='/test/vs/0', capability=coordinator.bound[0].capability, desc=desc)
|
||||||
|
|
||||||
|
with pytest.raises(ServiceValidationError, match="Remote control is turned off"):
|
||||||
|
await coordinator.async_send_command(bound, 'On')
|
||||||
@@ -34,6 +34,28 @@ def test_apply_writes_through_when_not_settling():
|
|||||||
assert mgr.cache.get('/oven/vs/0') == {'a': 1}
|
assert mgr.cache.get('/oven/vs/0') == {'a': 1}
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_merges_partial_update_onto_prior_rep():
|
||||||
|
"""Regression test for issue #27: a Bespoke fridge's /mode/vs/0 notify
|
||||||
|
(and even a later sweep entry for that href) can carry only `modes`,
|
||||||
|
omitting `supportedOptions` entirely. A full replace would wipe
|
||||||
|
`supportedOptions` from the cache the moment that partial update
|
||||||
|
arrives, even though the device's supported options didn't change --
|
||||||
|
which is exactly what made the flex-zone select disappear."""
|
||||||
|
mgr = _manager()
|
||||||
|
full = {
|
||||||
|
'x.com.samsung.da.modes': ['CVN_CONVERTIBLE_ZONE', 'CV_FDR_MEAT'],
|
||||||
|
'x.com.samsung.da.supportedOptions': ['CV_FDR_WINE', 'CV_FDR_MEAT'],
|
||||||
|
}
|
||||||
|
mgr.apply('/mode/vs/0', full, source='poll')
|
||||||
|
|
||||||
|
partial = {'x.com.samsung.da.modes': ['CVN_CONVERTIBLE_ZONE', 'WATERFILTER_ENABLE']}
|
||||||
|
mgr.apply('/mode/vs/0', partial, source='observe')
|
||||||
|
|
||||||
|
cached = mgr.cache.get('/mode/vs/0')
|
||||||
|
assert cached['x.com.samsung.da.modes'] == ['CVN_CONVERTIBLE_ZONE', 'WATERFILTER_ENABLE']
|
||||||
|
assert cached['x.com.samsung.da.supportedOptions'] == ['CV_FDR_WINE', 'CV_FDR_MEAT']
|
||||||
|
|
||||||
|
|
||||||
def test_apply_drops_update_during_settle_window():
|
def test_apply_drops_update_during_settle_window():
|
||||||
mgr = _manager()
|
mgr = _manager()
|
||||||
mgr.cache.apply_rep('/oven/vs/0', {'a': 1}, source='seed')
|
mgr.cache.apply_rep('/oven/vs/0', {'a': 1}, source='seed')
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ climate entity itself lives in climate.py (imports homeassistant) and is not
|
|||||||
importable here -- consistent with how the other HA platform files are untested.
|
importable here -- consistent with how the other HA platform files are untested.
|
||||||
"""
|
"""
|
||||||
from custom_components.localthings.registry.adapter import flatten
|
from custom_components.localthings.registry.adapter import flatten
|
||||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
from custom_components.localthings.registry.by_type import for_device, for_device_by_model
|
||||||
from custom_components.localthings.registry.capabilities import airconditioner
|
from custom_components.localthings.registry.capabilities import airconditioner
|
||||||
from custom_components.localthings.registry.discovery import discover
|
from custom_components.localthings.registry.discovery import discover
|
||||||
from custom_components.localthings.registry.entities import ClimateDesc
|
from custom_components.localthings.registry.entities import ClimateDesc
|
||||||
@@ -23,6 +23,22 @@ def _ac():
|
|||||||
return reg, resources
|
return reg, resources
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve(name):
|
||||||
|
"""Mirror the coordinator's detection order: oneUiVersion first, modelNum
|
||||||
|
fallback second (needed for issue #37's board, which reports neither
|
||||||
|
oneUiVersion nor a '_PRAC_' modelNum token)."""
|
||||||
|
resources = _load_device(name)
|
||||||
|
otn = resources.get('/otninformation/vs/0', {})
|
||||||
|
one_ui = otn.get('swVersionInfo', {}).get('oneUiVersion', '')
|
||||||
|
info = resources['/information/vs/0']
|
||||||
|
reg = for_device(one_ui) if one_ui else None
|
||||||
|
if reg is None:
|
||||||
|
reg = for_device_by_model(
|
||||||
|
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'],
|
||||||
|
)
|
||||||
|
return reg, resources
|
||||||
|
|
||||||
|
|
||||||
def _bound():
|
def _bound():
|
||||||
reg, resources = _ac()
|
reg, resources = _ac()
|
||||||
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
|
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
|
||||||
@@ -106,3 +122,114 @@ def test_climate_consumed_hrefs_declared_as_coverage():
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# TP1X_DA-AC-RAC-01011 (oneUiVersion "7.0 Air conditioner", Tizen Lite) -- a
|
||||||
|
# newer model class than the ARTIK051_PRAC dump above. It has no OCF-standard
|
||||||
|
# /temperature/current+desired pair (temperature lives on the vendor
|
||||||
|
# /temperatures/vs/0 items[] resource), exposes a /light/vs/0 display light, and
|
||||||
|
# carries extra vendor housekeeping hrefs. Issue #17 for this class (PR #36).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _ac_tp1x():
|
||||||
|
resources = _load_device('airconditioner_tp1x_da_ac_rac_01011')
|
||||||
|
one_ui = resources['/otninformation/vs/0']['swVersionInfo']['oneUiVersion']
|
||||||
|
return for_device(one_ui), resources
|
||||||
|
|
||||||
|
|
||||||
|
def test_tp1x_resolves_to_airconditioner_registry():
|
||||||
|
reg, _ = _ac_tp1x()
|
||||||
|
assert reg is not None and reg.name == 'airconditioner'
|
||||||
|
|
||||||
|
|
||||||
|
def test_tp1x_no_unbound_hrefs():
|
||||||
|
"""Every resource in the TP1X dump binds or is covered -- including
|
||||||
|
/temperatures/vs/0, /light/vs/0 and the housekeeping hrefs absent from
|
||||||
|
the ARTIK051 dump. Clears the coverage-gap repair."""
|
||||||
|
reg, resources = _ac_tp1x()
|
||||||
|
unbound = []
|
||||||
|
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||||
|
assert unbound == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_tp1x_display_light_switch_present():
|
||||||
|
"""/light/vs/0 (mode On/Off) surfaces as the display-light switch."""
|
||||||
|
reg, resources = _ac_tp1x()
|
||||||
|
state = flatten(
|
||||||
|
discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
|
||||||
|
assert state.get('display_light') is True # device reports mode == 'On'
|
||||||
|
|
||||||
|
|
||||||
|
def test_tp1x_vendor_temperature_and_light_covered():
|
||||||
|
"""The vendor temperature resource (read by the climate entity) and the
|
||||||
|
display-light resource both resolve in the registry -- no gap."""
|
||||||
|
reg, _ = _ac_tp1x()
|
||||||
|
assert reg.capabilities.get('/temperatures/vs/0'), '/temperatures/vs/0'
|
||||||
|
assert reg.capabilities.get('/light/vs/0'), '/light/vs/0'
|
||||||
|
|
||||||
|
|
||||||
|
def test_tp1x_climate_entity_is_bound():
|
||||||
|
"""The composite climate entity still binds the primary /mode/vs/0."""
|
||||||
|
reg, resources = _ac_tp1x()
|
||||||
|
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||||
|
climate = [b for b in bound if isinstance(b.desc, ClimateDesc)]
|
||||||
|
assert len(climate) == 1 and climate[0].href == '/mode/vs/0'
|
||||||
|
|
||||||
|
|
||||||
|
def test_tp2x_rac_20k_model_resolves_via_model_fallback():
|
||||||
|
"""TP2X_RAC_20K (issue #37) reports no oneUiVersion and no '_PRAC_' token
|
||||||
|
-- resolved via the '_RAC_' modelNum fallback added for this device."""
|
||||||
|
reg, _ = _resolve('airconditioner_tp2x_rac_20k')
|
||||||
|
assert reg is not None and reg.name == 'airconditioner'
|
||||||
|
|
||||||
|
|
||||||
|
def test_tp2x_rac_20k_no_unbound_hrefs():
|
||||||
|
reg, resources = _resolve('airconditioner_tp2x_rac_20k')
|
||||||
|
unbound = []
|
||||||
|
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||||
|
assert unbound == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_tp1x_rac_model_resolves_via_one_ui_version():
|
||||||
|
"""TP1X_DA-AC-RAC-01001_0000 (issue #38) self-reports oneUiVersion
|
||||||
|
'7.0 Air conditioner' -- resolved via for_device(), not the modelNum
|
||||||
|
fallback."""
|
||||||
|
reg, _ = _resolve('airconditioner_tp1x_rac')
|
||||||
|
assert reg is not None and reg.name == 'airconditioner'
|
||||||
|
|
||||||
|
|
||||||
|
def test_tp1x_rac_no_unbound_hrefs():
|
||||||
|
reg, resources = _resolve('airconditioner_tp1x_rac')
|
||||||
|
unbound = []
|
||||||
|
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||||
|
assert unbound == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_tp1x_rac_expected_state_keys_present():
|
||||||
|
reg, resources = _resolve('airconditioner_tp1x_rac')
|
||||||
|
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||||
|
state = flatten(bound, resources)
|
||||||
|
for key in ('climate', 'display_light', 'mute_once', 'selfcheck_status',
|
||||||
|
'selfcheck_result', 'current_limit_enabled', 'current_limit_level'):
|
||||||
|
assert key in state, key
|
||||||
|
|
||||||
|
|
||||||
|
def test_mute_once_write_target():
|
||||||
|
write = airconditioner.MUTE_ONCE.entities[0].write_fn
|
||||||
|
assert write('On', {}) == (['option', 'muteonce', 'vs', '0'], {'muteonce': 'On'})
|
||||||
|
assert write('Off', {}) == (['option', 'muteonce', 'vs', '0'], {'muteonce': 'Off'})
|
||||||
|
|
||||||
|
|
||||||
|
def test_display_light_write_target():
|
||||||
|
write = airconditioner.DISPLAY_LIGHT.entities[0].write_fn
|
||||||
|
assert write('On', {}) == (['light', 'vs', '0'], {'mode': 'On'})
|
||||||
|
assert write('Off', {}) == (['light', 'vs', '0'], {'mode': 'Off'})
|
||||||
|
|
||||||
|
|
||||||
|
def test_current_limit_is_read_only():
|
||||||
|
"""Meaning/write contract for the current-limit levels isn't confirmed
|
||||||
|
from the dump alone -- exposed as read-only diagnostic sensors rather
|
||||||
|
than a guessed writable control."""
|
||||||
|
for desc in airconditioner.CURRENT_LIMIT.entities:
|
||||||
|
assert getattr(desc, 'write_fn', None) is None
|
||||||
@@ -180,6 +180,26 @@ class TestForDeviceByModel:
|
|||||||
assert reg is not None
|
assert reg is not None
|
||||||
assert reg.name == 'airconditioner'
|
assert reg.name == 'airconditioner'
|
||||||
|
|
||||||
|
def test_washer_wf_prefix(self):
|
||||||
|
"""US front-load washers use the WF consumer-model prefix."""
|
||||||
|
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||||
|
reg = for_device_by_model(
|
||||||
|
'DA_WM_TP1_21_COMMON|20313741|20010001001611244AA3021700000000',
|
||||||
|
'DA_WM_TP1_21_COMMON_WF8900B/DC92-03129A_A0AE',
|
||||||
|
)
|
||||||
|
assert reg is not None
|
||||||
|
assert reg.name == 'washer'
|
||||||
|
|
||||||
|
def test_washer_wv_prefix(self):
|
||||||
|
"""FlexWash twin washers use the WV consumer-model prefix -- issue #19."""
|
||||||
|
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||||
|
reg = for_device_by_model(
|
||||||
|
'DA_WM_A51_20_COMMON|20198042|20020001001111400203000000000000',
|
||||||
|
'DA_WM_A51_20_COMMON_WV9600M/DC92-01980B_0014',
|
||||||
|
)
|
||||||
|
assert reg is not None
|
||||||
|
assert reg.name == 'washer'
|
||||||
|
|
||||||
def test_unknown_model_returns_none(self):
|
def test_unknown_model_returns_none(self):
|
||||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||||
reg = for_device_by_model('SOME-UNKNOWN-BOARD', 'SOME-UNKNOWN-BOARD')
|
reg = for_device_by_model('SOME-UNKNOWN-BOARD', 'SOME-UNKNOWN-BOARD')
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""Tests for the /temperatures/vs/0 fallback helper in climate.py (PR #36).
|
||||||
|
|
||||||
|
`_temps_vs_item` is a pure module-level function (plain dict in, plain dict
|
||||||
|
out) with no coordinator/entity dependency, so -- like select.py's `_display`
|
||||||
|
in test_select_display.py -- it's testable directly even though the
|
||||||
|
LocalThingsClimate entity itself needs a coordinator/hass fixture to
|
||||||
|
exercise. This covers the actual fallback-selection logic that the
|
||||||
|
registry-level "no unbound hrefs" tests in test_airconditioner_capabilities.py
|
||||||
|
don't reach: TP1X-class firmware has no OCF-standard
|
||||||
|
/temperature/current+desired pair, so current_temperature/target_temperature
|
||||||
|
would silently be None without this fallback.
|
||||||
|
"""
|
||||||
|
from custom_components.localthings.climate import _temps_vs_item
|
||||||
|
|
||||||
|
|
||||||
|
def test_temps_vs_item_returns_first_item():
|
||||||
|
rep = {
|
||||||
|
'x.com.samsung.da.items': [
|
||||||
|
{'x.com.samsung.da.current': '17.0', 'x.com.samsung.da.desired': '25.0'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
assert _temps_vs_item(rep) == {
|
||||||
|
'x.com.samsung.da.current': '17.0', 'x.com.samsung.da.desired': '25.0',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_temps_vs_item_empty_when_items_missing():
|
||||||
|
"""OCF-pair devices don't populate /temperatures/vs/0's items at all --
|
||||||
|
callers must fall through to the OCF hrefs cleanly, not raise."""
|
||||||
|
assert _temps_vs_item({}) == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_temps_vs_item_empty_when_items_is_empty_list():
|
||||||
|
assert _temps_vs_item({'x.com.samsung.da.items': []}) == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_temps_vs_item_empty_when_items_entry_is_not_a_dict():
|
||||||
|
assert _temps_vs_item({'x.com.samsung.da.items': ['not-a-dict']}) == {}
|
||||||
@@ -84,6 +84,14 @@ class TestRemoteControlFallback:
|
|||||||
assert common.REMOTE_CONTROL_VS_FALLBACK.match_fn(
|
assert common.REMOTE_CONTROL_VS_FALLBACK.match_fn(
|
||||||
{}, {'/remotectrl/0': {}, '/remotectrl/vs/0': {}}) is False
|
{}, {'/remotectrl/0': {}, '/remotectrl/vs/0': {}}) is False
|
||||||
|
|
||||||
|
def test_polled_warm_so_write_gating_stays_fresh(self):
|
||||||
|
"""coordinator.async_send_command blocks writes on this signal, so
|
||||||
|
it can't sit in the default 'cold' tier (refreshed only once per
|
||||||
|
30s summary poll) -- it needs the subscribe/subpoll cadence 'warm'
|
||||||
|
and 'hot' hrefs get instead."""
|
||||||
|
assert common.REMOTE_CONTROL_GENERIC.poll_tier == 'warm'
|
||||||
|
assert common.REMOTE_CONTROL_VS_FALLBACK.poll_tier == 'warm'
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Energy meter. instantaneousPower clamps negatives to 0, but the constant
|
# Energy meter. instantaneousPower clamps negatives to 0, but the constant
|
||||||
@@ -130,3 +138,202 @@ class TestEnergyMeter:
|
|||||||
phantom power sensor (the exists_fn replaces the field-presence gate)."""
|
phantom power sensor (the exists_fn replaces the field-presence gate)."""
|
||||||
pw = next(e for e in common.ENERGY_METER.entities if e.key == 'power_watts')
|
pw = next(e for e in common.ENERGY_METER.entities if e.key == 'power_watts')
|
||||||
assert pw.exists_fn({'x.com.samsung.da.cumulativePower': '5'}, {}) is False
|
assert pw.exists_fn({'x.com.samsung.da.cumulativePower': '5'}, {}) is False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# AI energy-saving level. '0' is off; supportedAiLevel lists the additional
|
||||||
|
# level(s) on offer. A single-entry list (issue #21 fridge, issue #40 washer)
|
||||||
|
# is really a binary toggle, so it's exposed as a switch instead of a
|
||||||
|
# one-choice select; multiple entries get a select with '0' synthesized back
|
||||||
|
# in as the explicit off option (supportedAiLevel never lists '0' itself, but
|
||||||
|
# it's a real, observed value of aiLevel).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _ai_energy_level_desc(cls_name):
|
||||||
|
return next(e for e in common.AI_ENERGY_LEVEL.entities
|
||||||
|
if e.__class__.__name__ == cls_name)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAiEnergyLevelSwitch:
|
||||||
|
def _desc(self):
|
||||||
|
return _ai_energy_level_desc('SwitchDesc')
|
||||||
|
|
||||||
|
def test_href(self):
|
||||||
|
assert common.AI_ENERGY_LEVEL.href == '/energy/ailevel/vs/0'
|
||||||
|
|
||||||
|
def test_shown_only_with_single_supported_level(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.exists_fn({'aiLevel': '1', 'supportedAiLevel': ['1']}, {}) is True
|
||||||
|
assert desc.exists_fn({'aiLevel': '1', 'supportedAiLevel': ['1', '2']}, {}) is False
|
||||||
|
|
||||||
|
def test_hidden_when_supported_level_is_non_list_scalar(self):
|
||||||
|
"""A stray scalar (e.g. a string) must not be len()-checked as if it
|
||||||
|
were a list -- a 2-char string would otherwise wrongly pass `== 1`
|
||||||
|
style checks."""
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.exists_fn({'aiLevel': '1', 'supportedAiLevel': '1'}, {}) is False
|
||||||
|
|
||||||
|
def test_hidden_when_supported_level_missing(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.exists_fn({'aiLevel': '1'}, {}) is False
|
||||||
|
|
||||||
|
def test_hidden_when_supported_level_empty_list(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.exists_fn({'aiLevel': '0', 'supportedAiLevel': []}, {}) is False
|
||||||
|
|
||||||
|
def test_hidden_on_empty_stub_rep(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.exists_fn({}, {}) is False
|
||||||
|
|
||||||
|
def test_value_fn(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.value_fn('0') is False
|
||||||
|
assert desc.value_fn('1') is True
|
||||||
|
|
||||||
|
def test_write_on_uses_the_single_supported_level(self):
|
||||||
|
"""The on-value is whatever the device calls its one level, not a
|
||||||
|
hardcoded '1'."""
|
||||||
|
desc = self._desc()
|
||||||
|
path, body = desc.write_fn('On', {'supportedAiLevel': ['2']})
|
||||||
|
assert path == ['energy', 'ailevel', 'vs', '0']
|
||||||
|
assert body == {'aiLevel': '2'}
|
||||||
|
|
||||||
|
def test_write_off(self):
|
||||||
|
desc = self._desc()
|
||||||
|
path, body = desc.write_fn('Off', {'supportedAiLevel': ['1']})
|
||||||
|
assert body == {'aiLevel': '0'}
|
||||||
|
|
||||||
|
|
||||||
|
class TestAiEnergyLevelSelect:
|
||||||
|
def _desc(self):
|
||||||
|
return _ai_energy_level_desc('SelectDesc')
|
||||||
|
|
||||||
|
def test_shown_only_with_multiple_supported_levels(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.exists_fn({'aiLevel': '1', 'supportedAiLevel': ['1', '2']}, {}) is True
|
||||||
|
assert desc.exists_fn({'aiLevel': '1', 'supportedAiLevel': ['1']}, {}) is False
|
||||||
|
|
||||||
|
def test_hidden_when_supported_level_is_non_list_scalar(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.exists_fn({'aiLevel': '1', 'supportedAiLevel': '12'}, {}) is False
|
||||||
|
|
||||||
|
def test_hidden_when_supported_level_missing(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.exists_fn({'aiLevel': '1'}, {}) is False
|
||||||
|
|
||||||
|
def test_hidden_when_supported_level_empty_list(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.exists_fn({'aiLevel': '0', 'supportedAiLevel': []}, {}) is False
|
||||||
|
|
||||||
|
def test_no_translation_key(self):
|
||||||
|
"""aiLevel's values are plain digits that render fine untranslated
|
||||||
|
(select.py's _display()) -- no strings.json entry to maintain
|
||||||
|
against an unknown number of future levels."""
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.translation_key is None
|
||||||
|
|
||||||
|
def test_options_synthesize_off(self):
|
||||||
|
"""'0' is never in supportedAiLevel but is a real, observed aiLevel
|
||||||
|
value -- synthesized back in as the explicit off option."""
|
||||||
|
desc = self._desc()
|
||||||
|
resources = {'/energy/ailevel/vs/0': {'supportedAiLevel': ['1', '2']}}
|
||||||
|
assert desc.options(resources) == ['0', '1', '2']
|
||||||
|
|
||||||
|
def test_options_empty_when_resource_missing(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.options({}) == ['0']
|
||||||
|
|
||||||
|
def test_write(self):
|
||||||
|
desc = self._desc()
|
||||||
|
path, body = desc.write_fn('2', {})
|
||||||
|
assert path == ['energy', 'ailevel', 'vs', '0']
|
||||||
|
assert body == {'aiLevel': '2'}
|
||||||
|
|
||||||
|
|
||||||
|
class TestAiEnergyLevelStubDoesNotDecideThePlatform:
|
||||||
|
"""Issue found in review: entity *creation* runs once, against whatever
|
||||||
|
snapshot is current when platforms are set up (see
|
||||||
|
__init__.py's async_config_entry_first_refresh-before-forward-entry-setups
|
||||||
|
ordering), while flatten() re-evaluates exists_fn every poll against live
|
||||||
|
data. Both descriptors share key='ai_energy_level' (see adapter._key), so
|
||||||
|
if a stub carve-out let one of them win at setup time while the other
|
||||||
|
wins once real data lands, flatten() would feed the already-instantiated
|
||||||
|
entity a value shaped for the other platform (e.g. a bool into a Select
|
||||||
|
expecting a string option). Neither side gets a `not rep` carve-out, so
|
||||||
|
an unfetched stub can never win entity creation for either platform --
|
||||||
|
the entity simply doesn't appear until a reload happens with real data,
|
||||||
|
same as any other exists_fn-gated entity in this codebase that's unlucky
|
||||||
|
on first-poll timing, instead of appearing as the wrong widget type."""
|
||||||
|
|
||||||
|
def test_neither_widget_exists_on_empty_stub_rep(self):
|
||||||
|
switch = _ai_energy_level_desc('SwitchDesc')
|
||||||
|
select = _ai_energy_level_desc('SelectDesc')
|
||||||
|
assert switch.exists_fn({}, {}) is False
|
||||||
|
assert select.exists_fn({}, {}) is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestSelfCheckError:
|
||||||
|
"""Self-check diagnostic error list -- surfaced on hardware that reports
|
||||||
|
x.com.samsung.da.error, joined into a single display string."""
|
||||||
|
|
||||||
|
def _desc(self):
|
||||||
|
return next(e for e in common.SELF_CHECK.entities if e.key == 'selfcheck_error')
|
||||||
|
|
||||||
|
def test_exists_when_field_present(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.exists_fn({'x.com.samsung.da.error': ['DA_ERROR_NONE']}, {}) is True
|
||||||
|
|
||||||
|
def test_does_not_exist_when_field_absent(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.exists_fn({'x.com.samsung.da.status': 'Ready'}, {}) is False
|
||||||
|
|
||||||
|
def test_exists_for_empty_stub_rep(self):
|
||||||
|
"""An empty {} rep is /device/0's not-yet-fetched-stub carve-out --
|
||||||
|
must be included-for-now, same as ENERGY_METER's fields."""
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.exists_fn({}, {}) is True
|
||||||
|
|
||||||
|
def test_value_joins_list(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.value_fn(['E1', 'E2']) == 'E1, E2'
|
||||||
|
|
||||||
|
def test_value_passes_through_scalar(self):
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.value_fn('DA_ERROR_NONE') == 'DA_ERROR_NONE'
|
||||||
|
|
||||||
|
def test_value_none_for_empty_list(self):
|
||||||
|
"""An empty error list means no value to show -- None (unknown),
|
||||||
|
not an empty string."""
|
||||||
|
desc = self._desc()
|
||||||
|
assert desc.value_fn([]) is None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cross-family bundles (UNIVERSAL / POWER) -- unpacked into every by_type
|
||||||
|
# registry's _build([...]) call in place of the hand-duplicated capability
|
||||||
|
# lists that used to live there.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestUniversalAndPowerBundles:
|
||||||
|
def test_universal_contains_the_no_conflict_capabilities(self):
|
||||||
|
assert set(common.UNIVERSAL) == {
|
||||||
|
common.ALARMS, common.ENERGY_METER, common.FIRMWARE_UPDATE,
|
||||||
|
common.SELF_CHECK, common.AI_ENERGY_LEVEL,
|
||||||
|
common.KIDS_LOCK_GENERIC, common.KIDS_LOCK_VS_FALLBACK,
|
||||||
|
common.REMOTE_CONTROL_GENERIC, common.REMOTE_CONTROL_VS_FALLBACK,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_power_kept_separate_for_airconditioners_sake(self):
|
||||||
|
"""See common.POWER's own comment for why airconditioner opts out."""
|
||||||
|
assert set(common.POWER) == {common.POWER_GENERIC, common.POWER_VS_FALLBACK}
|
||||||
|
|
||||||
|
def test_no_overlap_between_the_two_bundles(self):
|
||||||
|
assert not (set(common.UNIVERSAL) & set(common.POWER))
|
||||||
|
|
||||||
|
def test_airconditioner_registry_does_not_include_power(self):
|
||||||
|
from custom_components.localthings.registry.by_type import airconditioner
|
||||||
|
bound_caps = {c for caps in airconditioner.REGISTRY.capabilities.values() for c in caps}
|
||||||
|
assert common.POWER_GENERIC not in bound_caps
|
||||||
|
assert common.POWER_VS_FALLBACK not in bound_caps
|
||||||
@@ -158,6 +158,46 @@ def test_discover_match_fn_decline_is_not_logged_as_gap():
|
|||||||
assert seen == []
|
assert seen == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_discover_pattern_cap_reads_name_field():
|
||||||
|
"""A pattern cap's name_field normalizes a device-given instance name
|
||||||
|
(e.g. an ice maker's "CUBED_ICE") for use in the entity's display name,
|
||||||
|
independent of the href-derived key (issue #27)."""
|
||||||
|
cap = Capability(
|
||||||
|
href=None,
|
||||||
|
href_prefix='/icemaker/',
|
||||||
|
name_field='x.com.samsung.da.iceMaker.name',
|
||||||
|
entities=(BinarySensorDesc(key='enabled', field='x.com.samsung.da.iceMaker.state'),),
|
||||||
|
)
|
||||||
|
resources = {
|
||||||
|
'/icemaker/one/vs/0': {
|
||||||
|
'x.com.samsung.da.iceMaker.state': 'On',
|
||||||
|
'x.com.samsung.da.iceMaker.name': 'CUBED_ICE',
|
||||||
|
},
|
||||||
|
'/icemaker/two/vs/0': {
|
||||||
|
'x.com.samsung.da.iceMaker.state': 'On',
|
||||||
|
'x.com.samsung.da.iceMaker.name': 'ICE_BITES',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
bound = discover(resources, {}, pattern_caps=[cap])
|
||||||
|
names = {b.href: b.instance_name for b in bound}
|
||||||
|
assert names == {
|
||||||
|
'/icemaker/one/vs/0': 'Cubed Ice',
|
||||||
|
'/icemaker/two/vs/0': 'Ice Bites',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_discover_pattern_cap_name_field_absent_leaves_instance_name_none():
|
||||||
|
cap = Capability(
|
||||||
|
href=None,
|
||||||
|
href_prefix='/icemaker/',
|
||||||
|
name_field='x.com.samsung.da.iceMaker.name',
|
||||||
|
entities=(BinarySensorDesc(key='enabled', field='x.com.samsung.da.iceMaker.state'),),
|
||||||
|
)
|
||||||
|
resources = {'/icemaker/one/vs/0': {'x.com.samsung.da.iceMaker.state': 'On'}}
|
||||||
|
bound = discover(resources, {}, pattern_caps=[cap])
|
||||||
|
assert bound[0].instance_name is None
|
||||||
|
|
||||||
|
|
||||||
def test_discover_rt_filter_gates_binding():
|
def test_discover_rt_filter_gates_binding():
|
||||||
"""Cap with rt_filter must not bind a rep whose rt list does not match."""
|
"""Cap with rt_filter must not bind a rep whose rt list does not match."""
|
||||||
oven_mode_cap = Capability(
|
oven_mode_cap = Capability(
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Tests for LocalThingsEntity's display-name derivation
|
||||||
|
(custom_components/localthings/entity.py) -- the explicit-name,
|
||||||
|
device-given-instance-name, and href-derived fallbacks.
|
||||||
|
"""
|
||||||
|
from custom_components.localthings.entity import LocalThingsEntity
|
||||||
|
from custom_components.localthings.registry.capability import Capability
|
||||||
|
from custom_components.localthings.registry.discovery import BoundEntity
|
||||||
|
from custom_components.localthings.registry.entities import BinarySensorDesc
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeCoordinator:
|
||||||
|
device_serial = 'TEST-SERIAL'
|
||||||
|
|
||||||
|
def __init__(self, last_resources=None):
|
||||||
|
self.last_resources = last_resources or {}
|
||||||
|
|
||||||
|
|
||||||
|
def _make_entity(desc, href='/x/vs/0', key_override=None, instance='', instance_name=None):
|
||||||
|
capability = Capability(href=href, entities=(desc,))
|
||||||
|
bound = BoundEntity(href=href, capability=capability, desc=desc,
|
||||||
|
instance=instance, key_override=key_override,
|
||||||
|
instance_name=instance_name)
|
||||||
|
return LocalThingsEntity(_FakeCoordinator(), bound)
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_name_wins_over_everything():
|
||||||
|
desc = BinarySensorDesc(key='enabled', name='Explicit Name')
|
||||||
|
entity = _make_entity(desc, instance_name='Cubed Ice')
|
||||||
|
assert entity._attr_name == 'Explicit Name'
|
||||||
|
|
||||||
|
|
||||||
|
def test_instance_name_prefixes_the_derived_suffix():
|
||||||
|
"""Issue #27: an ice maker's device-given name ("Cubed Ice") replaces
|
||||||
|
the href-derived instance label ("Icemaker One") as the name prefix,
|
||||||
|
keeping the same entity-specific suffix."""
|
||||||
|
desc = BinarySensorDesc(key='enabled')
|
||||||
|
entity = _make_entity(desc, key_override='icemaker_one_enabled',
|
||||||
|
instance_name='Cubed Ice')
|
||||||
|
assert entity._attr_name == 'Cubed Ice Enabled'
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_instance_name_falls_back_to_derived_state_key():
|
||||||
|
desc = BinarySensorDesc(key='enabled')
|
||||||
|
entity = _make_entity(desc, key_override='icemaker_one_enabled')
|
||||||
|
assert entity._attr_name == 'Icemaker One Enabled'
|
||||||
@@ -102,3 +102,159 @@ class TestRefrigerationFallback:
|
|||||||
for key in ('rapid_fridge', 'rapid_freezing'):
|
for key in ('rapid_fridge', 'rapid_freezing'):
|
||||||
desc = next(e for e in fridge.REFRIGERATION_FALLBACK.entities if e.key == key)
|
desc = next(e for e in fridge.REFRIGERATION_FALLBACK.entities if e.key == key)
|
||||||
assert desc.exists_fn({}, {'/refrigeration/0': {}}) is True
|
assert desc.exists_fn({}, {'/refrigeration/0': {}}) is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestFlexZone:
|
||||||
|
"""Issue #32 (surfaced by #26/#27's TP2X/Bespoke dumps): the flex-zone
|
||||||
|
entry's prefix varies by fridge family (CV_TTYPE_RF9000A_ on RF9000-class,
|
||||||
|
CV_FDR_ on TP1X/Bespoke-class). The old hardcoded-prefix match left
|
||||||
|
CV_FDR_-family fridges with an always-unknown select, and a write would
|
||||||
|
have appended a duplicate flag instead of replacing the existing one."""
|
||||||
|
|
||||||
|
def test_reads_rf9000_prefix(self):
|
||||||
|
rep = {
|
||||||
|
'x.com.samsung.da.modes': ['CV_TTYPE_RF9000A_BEVERAGE', 'WATERFILTER_DISABLE'],
|
||||||
|
'x.com.samsung.da.supportedOptions': [
|
||||||
|
'CV_TTYPE_RF9000A_FREEZE', 'CV_TTYPE_RF9000A_BEVERAGE'],
|
||||||
|
}
|
||||||
|
assert fridge._flex_zone_current(rep) == 'CV_TTYPE_RF9000A_BEVERAGE'
|
||||||
|
|
||||||
|
def test_reads_cv_fdr_prefix(self):
|
||||||
|
rep = {
|
||||||
|
'x.com.samsung.da.modes': ['CVN_CONVERTIBLE_ZONE', 'CV_FDR_MEAT', 'WATERFILTER_ENABLE'],
|
||||||
|
'x.com.samsung.da.supportedOptions': [
|
||||||
|
'CV_FDR_WINE', 'CV_FDR_DELI', 'CV_FDR_BEVERAGE', 'CV_FDR_MEAT'],
|
||||||
|
}
|
||||||
|
assert fridge._flex_zone_current(rep) == 'CV_FDR_MEAT'
|
||||||
|
|
||||||
|
def test_write_replaces_rather_than_duplicates(self):
|
||||||
|
rep = {
|
||||||
|
'x.com.samsung.da.modes': ['CVN_CONVERTIBLE_ZONE', 'CV_FDR_MEAT', 'WATERFILTER_ENABLE'],
|
||||||
|
'x.com.samsung.da.supportedOptions': [
|
||||||
|
'CV_FDR_WINE', 'CV_FDR_DELI', 'CV_FDR_BEVERAGE', 'CV_FDR_MEAT'],
|
||||||
|
}
|
||||||
|
path, payload = fridge._flex_zone_write('CV_FDR_WINE', rep)
|
||||||
|
assert path == ['mode', 'vs', '0']
|
||||||
|
modes = payload['x.com.samsung.da.modes']
|
||||||
|
assert modes.count('CV_FDR_WINE') == 1
|
||||||
|
assert 'CV_FDR_MEAT' not in modes
|
||||||
|
assert 'CVN_CONVERTIBLE_ZONE' in modes and 'WATERFILTER_ENABLE' in modes
|
||||||
|
|
||||||
|
|
||||||
|
class TestTp1xNativeDuplicateResources:
|
||||||
|
"""The US TP1X_REF_21K publishes two native mirrors in addition to the
|
||||||
|
richer vendor resources. They must count as covered without producing
|
||||||
|
duplicate entities or guessing unverified write contracts."""
|
||||||
|
|
||||||
|
def test_duplicate_capabilities_have_no_entities(self):
|
||||||
|
assert fridge.DEFROST_DELAY_NATIVE_DUPLICATE.entities == ()
|
||||||
|
assert fridge.ICEMAKER_STATUS_NATIVE_DUPLICATE.entities == ()
|
||||||
|
|
||||||
|
def test_us_fixture_has_complete_coverage(self):
|
||||||
|
from custom_components.localthings.registry.adapter import flatten
|
||||||
|
from custom_components.localthings.registry.by_type import refrigerator
|
||||||
|
from custom_components.localthings.registry.discovery import discover
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
|
||||||
|
resources = _load_device('refrigerator_tp1x_ref_21k_us')
|
||||||
|
unbound = []
|
||||||
|
bound = discover(
|
||||||
|
resources,
|
||||||
|
refrigerator.REGISTRY.capabilities,
|
||||||
|
refrigerator.REGISTRY.pattern_capabilities,
|
||||||
|
log=unbound.append,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert unbound == []
|
||||||
|
state = flatten(bound, resources)
|
||||||
|
assert state['defrost_delay'] is False
|
||||||
|
assert 'ice_maker_enabled' not in state
|
||||||
|
assert state['icemaker_one_enabled'] is True
|
||||||
|
assert state['icemaker_two_enabled'] is True
|
||||||
|
assert state['selfcheck_error'] == 'DA_ERROR_NONE'
|
||||||
|
|
||||||
|
|
||||||
|
class TestPantryZone:
|
||||||
|
"""Cool Select Zone pantry compartment on ARTIK051_REF_17K -- issue #20."""
|
||||||
|
|
||||||
|
def test_href(self):
|
||||||
|
assert fridge.PANTRY_ZONE.href == '/status/pantry/one/vs/0'
|
||||||
|
|
||||||
|
def test_write(self):
|
||||||
|
desc = fridge.PANTRY_ZONE.entities[0]
|
||||||
|
path, body = desc.write_fn('FDR_WINE', {})
|
||||||
|
assert path == ['status', 'pantry', 'one', 'vs', '0']
|
||||||
|
assert body == {'x.com.samsung.da.mode': 'FDR_WINE'}
|
||||||
|
|
||||||
|
|
||||||
|
class TestArtik051AndTp2xFixturesHaveCompleteCoverage:
|
||||||
|
"""issue #20 (ARTIK051_REF_17K) and #26 (TP2X_REF_20K) both triggered
|
||||||
|
the incomplete-capability-coverage repair; both must resolve to zero
|
||||||
|
unbound hrefs now that /diagnosis/vs/0 (dishwasher.DIAGNOSIS, reused for
|
||||||
|
fridges), /status/pantry/one/vs/0 (PANTRY_ZONE), and the OCF-native
|
||||||
|
defrost/icemaker mirrors are covered."""
|
||||||
|
|
||||||
|
def test_artik051_ref_17k(self):
|
||||||
|
from custom_components.localthings.registry.adapter import flatten
|
||||||
|
from custom_components.localthings.registry.by_type import refrigerator
|
||||||
|
from custom_components.localthings.registry.discovery import discover
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
|
||||||
|
resources = _load_device('refrigerator_artik051_ref_17k')
|
||||||
|
unbound = []
|
||||||
|
bound = discover(
|
||||||
|
resources,
|
||||||
|
refrigerator.REGISTRY.capabilities,
|
||||||
|
refrigerator.REGISTRY.pattern_capabilities,
|
||||||
|
log=unbound.append,
|
||||||
|
)
|
||||||
|
assert unbound == []
|
||||||
|
state = flatten(bound, resources)
|
||||||
|
assert state['pantry_zone_mode'] == 'FDR_DRINKS'
|
||||||
|
|
||||||
|
def test_tp2x_ref_20k(self):
|
||||||
|
from custom_components.localthings.registry.adapter import flatten
|
||||||
|
from custom_components.localthings.registry.by_type import refrigerator
|
||||||
|
from custom_components.localthings.registry.discovery import discover
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
|
||||||
|
resources = _load_device('refrigerator_tp2x_ref_20k')
|
||||||
|
unbound = []
|
||||||
|
bound = discover(
|
||||||
|
resources,
|
||||||
|
refrigerator.REGISTRY.capabilities,
|
||||||
|
refrigerator.REGISTRY.pattern_capabilities,
|
||||||
|
log=unbound.append,
|
||||||
|
)
|
||||||
|
assert unbound == []
|
||||||
|
state = flatten(bound, resources)
|
||||||
|
assert state['flex_zone_mode'] == 'CV_FDR_BEVERAGE'
|
||||||
|
|
||||||
|
|
||||||
|
class TestRefrigeratorAiEnergyLevelFixtureCoverage:
|
||||||
|
"""AI energy-saving level (common.AI_ENERGY_LEVEL, see
|
||||||
|
test_common_capabilities.py) is exercised end-to-end here against the
|
||||||
|
refrigerator fixture/registry.
|
||||||
|
|
||||||
|
refrigerator_device.json's supportedAiLevel was extended to two entries
|
||||||
|
(['1', '2']) specifically to exercise the select branch -- the real
|
||||||
|
captured TP1X_REF_21K_US dump only ever reports one entry (switch
|
||||||
|
branch), so the select is synthetic-fixture-only for now."""
|
||||||
|
|
||||||
|
def test_synthetic_fixture_has_complete_coverage(self):
|
||||||
|
from custom_components.localthings.registry.adapter import flatten
|
||||||
|
from custom_components.localthings.registry.by_type import refrigerator
|
||||||
|
from custom_components.localthings.registry.discovery import discover
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
|
||||||
|
resources = _load_device('refrigerator')
|
||||||
|
unbound = []
|
||||||
|
bound = discover(
|
||||||
|
resources,
|
||||||
|
refrigerator.REGISTRY.capabilities,
|
||||||
|
refrigerator.REGISTRY.pattern_capabilities,
|
||||||
|
log=unbound.append,
|
||||||
|
)
|
||||||
|
assert unbound == []
|
||||||
|
state = flatten(bound, resources)
|
||||||
|
assert state['ai_energy_level'] == '1'
|
||||||
@@ -81,6 +81,141 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_reproduces_golden_state_keys_for_tp1x_ref_21k_us():
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
resources = _load_device('refrigerator_tp1x_ref_21k_us')
|
||||||
|
golden = json.loads(
|
||||||
|
(GOLDEN / 'refrigerator_tp1x_ref_21k_us.json').read_text()
|
||||||
|
)
|
||||||
|
state_keys = _new_state_keys('refrigerator_tp1x_ref_21k_us', 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_washer_flexwash():
|
||||||
|
"""FlexWash twin washers (WV-prefix consumer model, e.g. WV55M9600AW)
|
||||||
|
report no oneUiVersion and previously fell through for_device_by_model's
|
||||||
|
consumer-prefix map entirely -- issue #19."""
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
resources = _load_device('washer_flexwash')
|
||||||
|
golden = json.loads((GOLDEN / 'washer_flexwash.json').read_text())
|
||||||
|
state_keys = _new_state_keys('washer_flexwash', 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_washer_dryer_combo():
|
||||||
|
"""Washer/dryer combo units carry a writable dryLevel field on
|
||||||
|
/washer/vs/0 itself, with no separate dryer resource -- issue #22."""
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
resources = _load_device('washer_dryer_combo')
|
||||||
|
golden = json.loads((GOLDEN / 'washer_dryer_combo.json').read_text())
|
||||||
|
state_keys = _new_state_keys('washer_dryer_combo', resources)
|
||||||
|
assert set(state_keys) == set(golden['state_keys']), (
|
||||||
|
f"state_keys mismatch:\n"
|
||||||
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||||
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_reproduces_golden_state_keys_for_artik051_ref_17k():
|
||||||
|
"""ARTIK051_REF_17K's Cool Select Zone pantry compartment
|
||||||
|
(/status/pantry/one/vs/0) -- issue #20."""
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
resources = _load_device('refrigerator_artik051_ref_17k')
|
||||||
|
golden = json.loads((GOLDEN / 'refrigerator_artik051_ref_17k.json').read_text())
|
||||||
|
state_keys = _new_state_keys('refrigerator_artik051_ref_17k', resources)
|
||||||
|
assert set(state_keys) == set(golden['state_keys']), (
|
||||||
|
f"state_keys mismatch:\n"
|
||||||
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||||
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_reproduces_golden_state_keys_for_tp2x_ref_20k():
|
||||||
|
"""TP2X_REF_20K -- CV_FDR_-prefixed flex zone (issue #32) plus the extra
|
||||||
|
energy fields (cumulativeConsumption/monthlyConsumption/
|
||||||
|
thismonthlyConsumption) surfaced by issue #26."""
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
resources = _load_device('refrigerator_tp2x_ref_20k')
|
||||||
|
golden = json.loads((GOLDEN / 'refrigerator_tp2x_ref_20k.json').read_text())
|
||||||
|
state_keys = _new_state_keys('refrigerator_tp2x_ref_20k', 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_ac_tp1x_da_ac_rac_01011():
|
||||||
|
"""Newer AC firmware (Tizen Lite, oneUiVersion "7.0 Air conditioner"; model
|
||||||
|
TP1X_DA-AC-RAC-01011) reports temperature via the vendor /temperatures/vs/0
|
||||||
|
items[] resource and adds a /light/vs/0 display light, with extra vendor
|
||||||
|
housekeeping hrefs -- issue #17 for this model class (PR #36)."""
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
resources = _load_device('airconditioner_tp1x_da_ac_rac_01011')
|
||||||
|
golden = json.loads(
|
||||||
|
(GOLDEN / 'airconditioner_tp1x_da_ac_rac_01011.json').read_text()
|
||||||
|
)
|
||||||
|
state_keys = _new_state_keys('airconditioner_tp1x_da_ac_rac_01011', resources)
|
||||||
|
assert set(state_keys) == set(golden['state_keys']), (
|
||||||
|
f"state_keys mismatch:\n"
|
||||||
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||||
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_reproduces_golden_state_keys_for_tp2x_rac_20k():
|
||||||
|
"""TP2X_RAC_20K (issue #37) -- reports no oneUiVersion; resolved via the
|
||||||
|
'_RAC_' modelNum token fallback in for_device_by_model."""
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
resources = _load_device('airconditioner_tp2x_rac_20k')
|
||||||
|
golden = json.loads((GOLDEN / 'airconditioner_tp2x_rac_20k.json').read_text())
|
||||||
|
state_keys = _new_state_keys('airconditioner_tp2x_rac_20k', resources)
|
||||||
|
assert set(state_keys) == set(golden['state_keys']), (
|
||||||
|
f"state_keys mismatch:\n"
|
||||||
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||||
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_reproduces_golden_state_keys_for_tp1x_rac():
|
||||||
|
"""TP1X_DA-AC-RAC-01001_0000 (issue #38) -- fuller RAC board with display
|
||||||
|
light, self-check, mute-once, and a current-limit setting."""
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
resources = _load_device('airconditioner_tp1x_rac')
|
||||||
|
golden = json.loads((GOLDEN / 'airconditioner_tp1x_rac.json').read_text())
|
||||||
|
state_keys = _new_state_keys('airconditioner_tp1x_rac', resources)
|
||||||
|
assert set(state_keys) == set(golden['state_keys']), (
|
||||||
|
f"state_keys mismatch:\n"
|
||||||
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||||
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_reproduces_golden_state_keys_for_range():
|
||||||
|
"""Range/cooktop-oven combo (model TP1X_DA-KS-RANGE-0102X, issue #44) --
|
||||||
|
reports no oneUiVersion; resolved via the '-RANGE-' modelNum token
|
||||||
|
fallback in for_device_by_model. Reuses the oven family's cavity/
|
||||||
|
setpoint/mode/operational-state capabilities and adds range.py's
|
||||||
|
per-burner capabilities for the 4 burners this dump reports."""
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
resources = _load_device('range')
|
||||||
|
golden = json.loads((GOLDEN / 'range.json').read_text())
|
||||||
|
state_keys = _new_state_keys('range', resources)
|
||||||
|
assert set(state_keys) == set(golden['state_keys']), (
|
||||||
|
f"state_keys mismatch:\n"
|
||||||
|
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||||
|
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_resources_from_batch_preferred_over_flat():
|
def test_resources_from_batch_preferred_over_flat():
|
||||||
from tests.conftest import _resources_from_dump
|
from tests.conftest import _resources_from_dump
|
||||||
dump = {
|
dump = {
|
||||||
|
|||||||
@@ -53,6 +53,46 @@ def test_oven_setpoint_rejects_missing_items():
|
|||||||
assert desc.write_fn(200, {}) is None
|
assert desc.write_fn(200, {}) is None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# OVEN_SETPOINT — Fahrenheit bounds (issue #44 range dump reports
|
||||||
|
# unit="Fahrenheit"; bounds/step must track the live unit, not stay pinned
|
||||||
|
# to the Celsius defaults)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _fahrenheit_rep(desired='0'):
|
||||||
|
return {'x.com.samsung.da.items': [{
|
||||||
|
'x.com.samsung.da.desired': desired,
|
||||||
|
'x.com.samsung.da.unit': 'Fahrenheit',
|
||||||
|
}]}
|
||||||
|
|
||||||
|
|
||||||
|
def test_oven_setpoint_write_uses_fahrenheit_bounds():
|
||||||
|
desc = oven.OVEN_SETPOINT.entities[0]
|
||||||
|
rep = _fahrenheit_rep()
|
||||||
|
# 350 is within F bounds (175-550) but above the C max (270) --
|
||||||
|
# confirms the write path isn't silently still clamping to Celsius.
|
||||||
|
_, body = desc.write_fn(350, rep)
|
||||||
|
assert body['x.com.samsung.da.items'][0]['x.com.samsung.da.desired'] == '350'
|
||||||
|
|
||||||
|
|
||||||
|
def test_oven_setpoint_rejects_out_of_range_fahrenheit():
|
||||||
|
desc = oven.OVEN_SETPOINT.entities[0]
|
||||||
|
rep = _fahrenheit_rep()
|
||||||
|
assert desc.write_fn(100, rep) is None # below F min (175)
|
||||||
|
assert desc.write_fn(600, rep) is None # above F max (550)
|
||||||
|
|
||||||
|
|
||||||
|
def test_oven_setpoint_native_bounds_track_live_unit():
|
||||||
|
desc = oven.OVEN_SETPOINT.entities[0]
|
||||||
|
celsius_rep = {'x.com.samsung.da.items': [{'x.com.samsung.da.unit': 'Celsius'}]}
|
||||||
|
assert desc.native_min_fn(celsius_rep) == 30.0
|
||||||
|
assert desc.native_max_fn(celsius_rep) == 270.0
|
||||||
|
fahrenheit_rep = _fahrenheit_rep()
|
||||||
|
assert desc.native_min_fn(fahrenheit_rep) == 175.0
|
||||||
|
assert desc.native_max_fn(fahrenheit_rep) == 550.0
|
||||||
|
assert desc.step_fn(fahrenheit_rep) == 5.0
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# OVEN_MODE — SelectDesc with non-empty options
|
# OVEN_MODE — SelectDesc with non-empty options
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"""Tests for range/cooktop-oven combo support (issue #44)."""
|
||||||
|
from custom_components.localthings.registry.adapter import flatten
|
||||||
|
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||||
|
from custom_components.localthings.registry.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')
|
||||||
|
info = resources['/information/vs/0']
|
||||||
|
reg = for_device_by_model(
|
||||||
|
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'],
|
||||||
|
)
|
||||||
|
return reg, resources
|
||||||
|
|
||||||
|
|
||||||
|
def _state():
|
||||||
|
reg, resources = _range()
|
||||||
|
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||||
|
return flatten(bound, resources)
|
||||||
|
|
||||||
|
|
||||||
|
def test_range_model_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 #44 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_expected_entities_present():
|
||||||
|
state = _state()
|
||||||
|
for key in (
|
||||||
|
'power_switch', 'oven_setpoint', 'current_temp_c', 'oven_mode',
|
||||||
|
'machine_state', 'door_open', 'cloud_connected', 'cooktop_state',
|
||||||
|
'cooktop_safety_shutoff_enabled',
|
||||||
|
'burner_0_power_level', 'burner_0_state', 'burner_0_hot_surface',
|
||||||
|
'burner_3_power_level', 'burner_3_state', 'burner_3_hot_surface',
|
||||||
|
):
|
||||||
|
assert key in state, key
|
||||||
|
|
||||||
|
|
||||||
|
def test_unreported_burners_gated_out():
|
||||||
|
"""Dump reports numberOfBurners=4 (indices 0-3) -- burner slots 4/5 of
|
||||||
|
MAX_BURNERS must not appear as entities."""
|
||||||
|
state = _state()
|
||||||
|
for key in ('burner_4_power_level', 'burner_5_power_level'):
|
||||||
|
assert key not in state, key
|
||||||
|
|
||||||
|
|
||||||
|
def test_burner_power_level_write_is_read_modify_write():
|
||||||
|
desc = next(e for e in range_caps.COOKTOP_STATUS.entities
|
||||||
|
if e.key == 'burner_1_power_level')
|
||||||
|
rep = {'burnerList': [
|
||||||
|
{'burnerNumber': 0, 'powerLevel': '3'},
|
||||||
|
{'burnerNumber': 1, 'powerLevel': '0'},
|
||||||
|
]}
|
||||||
|
path, body = desc.write_fn('boost', rep)
|
||||||
|
assert path == ['cooktop', 'status', 'vs', '0']
|
||||||
|
burners = {b['burnerNumber']: b['powerLevel'] for b in body['burnerList']}
|
||||||
|
assert burners[1] == 'boost'
|
||||||
|
assert burners[0] == '3' # sibling burner untouched
|
||||||
|
|
||||||
|
|
||||||
|
def test_burner_power_level_write_rejects_missing_burner():
|
||||||
|
desc = next(e for e in range_caps.COOKTOP_STATUS.entities
|
||||||
|
if e.key == 'burner_2_power_level')
|
||||||
|
rep = {'burnerList': [{'burnerNumber': 0, 'powerLevel': '0'}]}
|
||||||
|
assert desc.write_fn('5', rep) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_burner_hot_surface_true_when_not_normal():
|
||||||
|
desc = next(e for e in range_caps.COOKTOP_STATUS.entities
|
||||||
|
if e.key == 'burner_0_hot_surface')
|
||||||
|
assert desc.value_fn([{'burnerNumber': 0, 'hotSurfaceState': 'hot'}]) is True
|
||||||
|
assert desc.value_fn([{'burnerNumber': 0, 'hotSurfaceState': 'normal'}]) is False
|
||||||
@@ -40,6 +40,22 @@ class TestWasherSettings:
|
|||||||
assert body == {'x.com.samsung.da.rinseCycles': '3'}
|
assert body == {'x.com.samsung.da.rinseCycles': '3'}
|
||||||
|
|
||||||
|
|
||||||
|
class TestDryLevel:
|
||||||
|
"""Washer/dryer combo units carry a writable dryLevel field on
|
||||||
|
/washer/vs/0 itself, self-gated off on plain washers -- issue #22."""
|
||||||
|
|
||||||
|
def test_exists_only_when_supported_dry_level_present(self):
|
||||||
|
desc = next(e for e in washer.WASHER_SETTINGS.entities if e.key == 'dry_level')
|
||||||
|
assert desc.exists_fn({'x.com.samsung.da.supportedDryLevel': ['None', '30']}, {}) is True
|
||||||
|
assert desc.exists_fn({}, {}) is False
|
||||||
|
|
||||||
|
def test_write(self):
|
||||||
|
desc = next(e for e in washer.WASHER_SETTINGS.entities if e.key == 'dry_level')
|
||||||
|
path, body = desc.write_fn('Cupboard', {})
|
||||||
|
assert path == ['washer', 'vs', '0']
|
||||||
|
assert body == {'x.com.samsung.da.dryLevel': 'Cupboard'}
|
||||||
|
|
||||||
|
|
||||||
class TestWasherCourse:
|
class TestWasherCourse:
|
||||||
def test_href(self):
|
def test_href(self):
|
||||||
assert washer.WASHER_COURSE.href == '/course/vs/0'
|
assert washer.WASHER_COURSE.href == '/course/vs/0'
|
||||||
@@ -233,3 +249,201 @@ class TestDetergentSoftenerDosing:
|
|||||||
assert self._desc('detergent_low').exists_fn({'x.com.samsung.da.options': []}, {}) is False
|
assert self._desc('detergent_low').exists_fn({'x.com.samsung.da.options': []}, {}) is False
|
||||||
rep = {'x.com.samsung.da.options': _DOSING_OPTIONS}
|
rep = {'x.com.samsung.da.options': _DOSING_OPTIONS}
|
||||||
assert self._desc('detergent_low').exists_fn(rep, {}) is True
|
assert self._desc('detergent_low').exists_fn(rep, {}) is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestWashOptionToggles:
|
||||||
|
"""Bubble soak / pre-wash / intensive-wash switches, from the same
|
||||||
|
options[] array as the cycle select (issue #22 follow-up). Confirmed
|
||||||
|
On/Off shape from a dump with Bubble Soak toggled on in the app."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _desc(key):
|
||||||
|
return next(e for e in washer.WASHER_COURSE.entities if e.key == key)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _keys():
|
||||||
|
return ('bubble_soak', 'pre_wash', 'intensive')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _prefix(key):
|
||||||
|
return {'bubble_soak': 'BubbleSoak',
|
||||||
|
'pre_wash': 'PreWashSetting',
|
||||||
|
'intensive': 'IntensiveSetting'}[key]
|
||||||
|
|
||||||
|
def test_exists_only_when_field_present(self):
|
||||||
|
for key in self._keys():
|
||||||
|
desc = self._desc(key)
|
||||||
|
assert desc.exists_fn({'x.com.samsung.da.options': []}, {}) is False
|
||||||
|
prefix = self._prefix(key)
|
||||||
|
rep = {'x.com.samsung.da.options': [f'{prefix}_Off']}
|
||||||
|
assert desc.exists_fn(rep, {}) is True
|
||||||
|
|
||||||
|
def test_reads_off(self):
|
||||||
|
for key in self._keys():
|
||||||
|
prefix = self._prefix(key)
|
||||||
|
rep = {'x.com.samsung.da.options': [f'{prefix}_Off']}
|
||||||
|
assert self._desc(key).rep_fn(rep) is False
|
||||||
|
|
||||||
|
def test_reads_on(self):
|
||||||
|
for key in self._keys():
|
||||||
|
prefix = self._prefix(key)
|
||||||
|
rep = {'x.com.samsung.da.options': [f'{prefix}_On']}
|
||||||
|
assert self._desc(key).rep_fn(rep) is True
|
||||||
|
|
||||||
|
def test_write_on_and_off(self):
|
||||||
|
"""write_fn receives the same 'On'/'Off' string switch.py sends
|
||||||
|
(not a bool) -- covers a bug where an earlier `'On' if p else 'Off'`
|
||||||
|
implementation always wrote 'On', since any non-empty string
|
||||||
|
(including 'Off') is truthy."""
|
||||||
|
for key in self._keys():
|
||||||
|
prefix = self._prefix(key)
|
||||||
|
rep = {'x.com.samsung.da.options': [f'{prefix}_Off', 'GMT_02']}
|
||||||
|
path, body = self._desc(key).write_fn('On', rep)
|
||||||
|
assert path == ['course', 'vs', '0']
|
||||||
|
assert f'{prefix}_On' in body['x.com.samsung.da.options']
|
||||||
|
assert 'GMT_02' in body['x.com.samsung.da.options']
|
||||||
|
|
||||||
|
rep = {'x.com.samsung.da.options': [f'{prefix}_On']}
|
||||||
|
path, body = self._desc(key).write_fn('Off', rep)
|
||||||
|
assert f'{prefix}_Off' in body['x.com.samsung.da.options']
|
||||||
|
assert f'{prefix}_On' not in body['x.com.samsung.da.options']
|
||||||
|
|
||||||
|
def test_write_rejects_non_on_off_payload(self):
|
||||||
|
for key in self._keys():
|
||||||
|
prefix = self._prefix(key)
|
||||||
|
rep = {'x.com.samsung.da.options': [f'{prefix}_Off']}
|
||||||
|
assert self._desc(key).write_fn('bogus', rep) is None
|
||||||
|
|
||||||
|
|
||||||
|
# editCourseList and availability bitmaps from the reporter's issue #22
|
||||||
|
# follow-up dump (WD90T654DBN/S1, course '30' selected, Bubble Soak just
|
||||||
|
# turned on in the app): 24 courses, course '30' at position 1 reads 'F0'
|
||||||
|
# (available) on all three bitmaps; course '1C' at position 0 reads '00' on
|
||||||
|
# BubbleSoakSet (matching the app graying that control out for Eco 40-60).
|
||||||
|
_EDIT_COURSE_RESOURCES = {
|
||||||
|
'/wm/editcourse/vs/0': {
|
||||||
|
'x.com.samsung.da.editCourseList':
|
||||||
|
'EditCourseList_1C301E26361B1D1F253324322022232F212D272838393729',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_BUBBLE_SOAK_SET = 'BubbleSoakSet_00F000F000F000F0F0F0F00000F000F0F00000F000000000'
|
||||||
|
_PRE_WASH_AVAILABLE_SET = 'PreWashAvailableSet_F0F000F0F0F000F0F0F0F00000F0F0F0F00000F000000000'
|
||||||
|
_INTENSIVE_AVAILABLE_SET = 'IntensiveAvailableSet_F0F000F0F0F000F0F0F0F00000F0F0F0F00000F000000000'
|
||||||
|
|
||||||
|
|
||||||
|
class TestWashOptionToggleValidation:
|
||||||
|
"""validate_fn rejects turning a toggle on for a course whose byte in
|
||||||
|
its availability bitmap isn't 'F0', with a user-facing message switch.py
|
||||||
|
raises as ServiceValidationError -- distinct from write_fn's silent
|
||||||
|
no-op for a malformed payload."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _desc(key):
|
||||||
|
return next(e for e in washer.WASHER_COURSE.entities if e.key == key)
|
||||||
|
|
||||||
|
def test_allowed_on_a_supported_course(self):
|
||||||
|
rep = {'x.com.samsung.da.options': ['Course_30', _BUBBLE_SOAK_SET]}
|
||||||
|
assert self._desc('bubble_soak').validate_fn(
|
||||||
|
'On', rep, _EDIT_COURSE_RESOURCES) is None
|
||||||
|
|
||||||
|
def test_rejected_on_an_unsupported_course(self):
|
||||||
|
rep = {'x.com.samsung.da.options': ['Course_1C', _BUBBLE_SOAK_SET]}
|
||||||
|
msg = self._desc('bubble_soak').validate_fn('On', rep, _EDIT_COURSE_RESOURCES)
|
||||||
|
assert msg == "Bubble soak isn't available on the selected cycle."
|
||||||
|
|
||||||
|
def test_pre_wash_and_intensive_use_their_own_availableset_field(self):
|
||||||
|
rep = {'x.com.samsung.da.options': ['Course_30', _PRE_WASH_AVAILABLE_SET]}
|
||||||
|
assert self._desc('pre_wash').validate_fn(
|
||||||
|
'On', rep, _EDIT_COURSE_RESOURCES) is None
|
||||||
|
rep = {'x.com.samsung.da.options': ['Course_30', _INTENSIVE_AVAILABLE_SET]}
|
||||||
|
assert self._desc('intensive').validate_fn(
|
||||||
|
'On', rep, _EDIT_COURSE_RESOURCES) is None
|
||||||
|
|
||||||
|
def test_turning_off_is_never_blocked(self):
|
||||||
|
rep = {'x.com.samsung.da.options': ['Course_1C', _BUBBLE_SOAK_SET]}
|
||||||
|
assert self._desc('bubble_soak').validate_fn(
|
||||||
|
'Off', rep, _EDIT_COURSE_RESOURCES) is None
|
||||||
|
|
||||||
|
def test_allows_write_when_course_unresolvable(self):
|
||||||
|
"""No editCourseList, no Course_ token, or a bitmap whose length
|
||||||
|
doesn't match editCourseList -- in every case, fail open rather than
|
||||||
|
block a write we can't actually verify."""
|
||||||
|
desc = self._desc('bubble_soak')
|
||||||
|
rep = {'x.com.samsung.da.options': ['Course_1C', _BUBBLE_SOAK_SET]}
|
||||||
|
assert desc.validate_fn('On', rep, {}) is None
|
||||||
|
|
||||||
|
rep = {'x.com.samsung.da.options': [_BUBBLE_SOAK_SET]}
|
||||||
|
assert desc.validate_fn('On', rep, _EDIT_COURSE_RESOURCES) is None
|
||||||
|
|
||||||
|
rep = {'x.com.samsung.da.options': ['Course_1C', 'BubbleSoakSet_00F0']}
|
||||||
|
assert desc.validate_fn('On', rep, _EDIT_COURSE_RESOURCES) is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestAiEnergyLevel:
|
||||||
|
"""Issue #40 -- /energy/ailevel/vs/0 was unbound on a plain washer.
|
||||||
|
|
||||||
|
The capability itself (common.AI_ENERGY_LEVEL) is tested in
|
||||||
|
test_common_capabilities.py; this just confirms it's wired into the
|
||||||
|
washer registry and that the fixture's single-entry supportedAiLevel
|
||||||
|
(['1'], matching the issue's dump) surfaces as a switch, not a select."""
|
||||||
|
|
||||||
|
def test_fixture_has_complete_coverage(self):
|
||||||
|
from custom_components.localthings.registry.adapter import flatten
|
||||||
|
from custom_components.localthings.registry.by_type import washer as washer_registry
|
||||||
|
from custom_components.localthings.registry.discovery import discover
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
|
||||||
|
resources = _load_device('washer')
|
||||||
|
unbound = []
|
||||||
|
bound = discover(
|
||||||
|
resources,
|
||||||
|
washer_registry.REGISTRY.capabilities,
|
||||||
|
washer_registry.REGISTRY.pattern_capabilities,
|
||||||
|
log=unbound.append,
|
||||||
|
)
|
||||||
|
assert unbound == []
|
||||||
|
state = flatten(bound, resources)
|
||||||
|
assert state['ai_energy_level'] is False # aiLevel '0' -- off
|
||||||
|
|
||||||
|
|
||||||
|
class TestFlexWashAndComboFixturesHaveCompleteCoverage:
|
||||||
|
"""FlexWash (issue #19, previously unrecognized entirely) and
|
||||||
|
washer/dryer combo (issue #22, dry_level) dumps must both resolve to
|
||||||
|
zero unbound hrefs."""
|
||||||
|
|
||||||
|
def test_flexwash(self):
|
||||||
|
from custom_components.localthings.registry.adapter import flatten
|
||||||
|
from custom_components.localthings.registry.by_type import washer as washer_registry
|
||||||
|
from custom_components.localthings.registry.discovery import discover
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
|
||||||
|
resources = _load_device('washer_flexwash')
|
||||||
|
unbound = []
|
||||||
|
bound = discover(
|
||||||
|
resources,
|
||||||
|
washer_registry.REGISTRY.capabilities,
|
||||||
|
washer_registry.REGISTRY.pattern_capabilities,
|
||||||
|
log=unbound.append,
|
||||||
|
)
|
||||||
|
assert unbound == []
|
||||||
|
state = flatten(bound, resources)
|
||||||
|
assert 'dry_level' not in state # plain washer -- no supportedDryLevel field
|
||||||
|
|
||||||
|
def test_dryer_combo(self):
|
||||||
|
from custom_components.localthings.registry.adapter import flatten
|
||||||
|
from custom_components.localthings.registry.by_type import washer as washer_registry
|
||||||
|
from custom_components.localthings.registry.discovery import discover
|
||||||
|
from tests.conftest import _load_device
|
||||||
|
|
||||||
|
resources = _load_device('washer_dryer_combo')
|
||||||
|
unbound = []
|
||||||
|
bound = discover(
|
||||||
|
resources,
|
||||||
|
washer_registry.REGISTRY.capabilities,
|
||||||
|
washer_registry.REGISTRY.pattern_capabilities,
|
||||||
|
log=unbound.append,
|
||||||
|
)
|
||||||
|
assert unbound == []
|
||||||
|
state = flatten(bound, resources)
|
||||||
|
# the device's own "off" sentinel is the literal string 'None', not absence
|
||||||
|
assert state['dry_level'] == 'None'
|
||||||
Reference in new issue
Block a user