diff --git a/README.md b/README.md index 0c9c89e..e9881b3 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Your state stays on your LAN: HA talks to the appliance over a direct DTLS sessi | Air purifier | `by_type/air_purifier.py` | | Dehumidifier | `by_type/dehumidifier.py` | | Dryer | `by_type/dryer.py` | -| Oven | `by_type/oven.py` | +| Oven (including combi microwaves) | `by_type/oven.py` | | Gas cooktop (read-only burner status) | `by_type/cooktop.py` | | Range hood | `by_type/range_hood.py` | | Range | `by_type/range.py` | @@ -33,6 +33,7 @@ Your state stays on your LAN: HA talks to the appliance over a direct DTLS sessi | Refrigerator | `by_type/refrigerator.py` | | Washer | `by_type/washer.py` | | Water purifier | `by_type/water_purifier.py` | +| Vacuum clean/auto-empty station | `by_type/vacuum_station.py` | Each registry composes shared and family-specific `Capability` objects from `registry/capabilities/`; those modules document the individual resources/entities in more depth than a README table can stay current with. diff --git a/custom_components/localthings/climate.py b/custom_components/localthings/climate.py index 83ec75d..43e9aff 100644 --- a/custom_components/localthings/climate.py +++ b/custom_components/localthings/climate.py @@ -48,6 +48,7 @@ from .registry.capabilities.airconditioner import ( HREF_TEMPS_VS as TEMPS_VS_HREF, HREF_WIND_STRENGTH as WIND_STRENGTH_HREF, HREF_WIND_DIRECTION as WIND_DIRECTION_HREF, + HREF_WIND_OSCILLATION as WIND_OSCILLATION_HREF, HREF_CONVENIENT as CONVENIENT_HREF, ) from .registry.capabilities.common import normalize_temp_unit @@ -120,6 +121,26 @@ _DEVICE_TO_SWING: dict[str, str] = { } _SWING_TO_DEVICE = {v: k for k, v in _DEVICE_TO_SWING.items()} +# Swing fallback via /wind/oscillation/vs/0 (issue #126) -- boards without +# WIND_DIRECTION_HREF at all report two independent Swing|Fix toggles +# instead of one combined code. Same HA vocabulary as _DEVICE_TO_SWING +# above (off/vertical/horizontal/both), just read from/written to a pair +# of fields rather than a single one. +def _oscillation_swing(rep: dict) -> str | None: + vertical = rep.get('vertical') + horizontal = rep.get('horizontal') + if vertical is None and horizontal is None: + return None + v = vertical == 'Swing' + h = horizontal == 'Swing' + if v and h: + return 'both' + if v: + return 'vertical' + if h: + return 'horizontal' + return 'off' + # Preset (convenient mode): resolved dynamically from the device's own # /mode/convenient/vs/0 supportedModes -- no per-model table. The device 'Off' # code maps to HA's PRESET_NONE ("no preset active"); every other code is @@ -358,13 +379,25 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity): def fan_modes(self) -> list[str]: return self._read_modes(WIND_STRENGTH_HREF, _DEVICE_TO_FAN) + def _swing_via_direction(self) -> bool: + """True when WIND_DIRECTION_HREF is the swing channel to use -- + signalled by its presence. Boards without it (issue #126) report + the 2-axis oscillation resource instead; see _oscillation_swing.""" + return bool(self._rep(WIND_DIRECTION_HREF)) + @property def swing_mode(self): - return self._read_mode(WIND_DIRECTION_HREF, _DEVICE_TO_SWING) + if self._swing_via_direction(): + return self._read_mode(WIND_DIRECTION_HREF, _DEVICE_TO_SWING) + return _oscillation_swing(self._rep(WIND_OSCILLATION_HREF)) @property def swing_modes(self) -> list[str]: - return self._read_modes(WIND_DIRECTION_HREF, _DEVICE_TO_SWING) + if self._swing_via_direction(): + return self._read_modes(WIND_DIRECTION_HREF, _DEVICE_TO_SWING) + if self._rep(WIND_OSCILLATION_HREF): + return list(_SWING_TO_DEVICE.keys()) + return [] @property def preset_mode(self): @@ -438,7 +471,12 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity): await self._set_mapped('fan', _FAN_TO_DEVICE, fan_mode) async def async_set_swing_mode(self, swing_mode: str) -> None: - await self._set_mapped('swing', _SWING_TO_DEVICE, swing_mode) + if self._swing_via_direction(): + await self._set_mapped('swing', _SWING_TO_DEVICE, swing_mode) + return + if self._rep(WIND_OSCILLATION_HREF): + await self.coordinator.async_send_command( + self._bound, ('oscillation', swing_mode)) async def async_set_preset_mode(self, preset_mode: str) -> None: if preset_mode == PRESET_AI_COMFORT: diff --git a/custom_components/localthings/coordinator.py b/custom_components/localthings/coordinator.py index dc9e960..27e6c7e 100644 --- a/custom_components/localthings/coordinator.py +++ b/custom_components/localthings/coordinator.py @@ -85,6 +85,24 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): _OBSERVE_GRACE_PERIOD_S: float = GRACE_PERIOD_S _RECONNECT_PAUSE_S: float = 5.0 + # A single reconnect is normal appliance-side behavior (see the + # README's "Known device behavior" section) -- Samsung's firmware + # drops the DTLS session briefly every now and then, and the + # coordinator recovering from that on its own isn't something a user + # needs to see at WARNING. Only escalate once reconnects pile up + # within a trailing window (issue #119). + # + # The window can't be a literal 60s: consecutive reconnect attempts are + # never closer together than one summary poll interval (SUMMARY_INTERVAL_S, + # 30s) plus _RECONNECT_PAUSE_S, so at most ~2 can ever land inside a 60s + # window regardless of how unhealthy the connection is -- a threshold of + # 5 there could never fire, silently downgrading every reconnect + # (including a persistently broken one) to INFO forever. 300s/3 instead: + # reachable under normal polling, and 3 reconnects inside 5 minutes is + # still a reasonable proxy for the README's "actually broken" case. + _RECONNECT_WARN_WINDOW_S: float = 300.0 + _RECONNECT_WARN_THRESHOLD: int = 3 + # A block-level ACK timeout on the summary GET doesn't prove the # session is dead (see _poll_once) — require this many in a row # before treating it as one. A single slow transfer on an otherwise @@ -136,6 +154,7 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.one_ui_version: str = '' self._consecutive_poll_timeouts = 0 self._unbound_hrefs: list[str] = [] + self._reconnect_times: list[float] = [] # ------------------------------------------------------------------ # Session management (all blocking — must run in executor) @@ -444,6 +463,17 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): self._consecutive_poll_timeouts += 1 return self._consecutive_poll_timeouts < self._POLL_TIMEOUT_LIMIT + def _reconnect_is_frequent(self) -> bool: + """True once this cycle's reconnect is the Nth within the trailing + warn window -- see _RECONNECT_WARN_WINDOW_S/_RECONNECT_WARN_THRESHOLD.""" + now = time.monotonic() + self._reconnect_times = [ + t for t in self._reconnect_times + if now - t < self._RECONNECT_WARN_WINDOW_S + ] + self._reconnect_times.append(now) + return len(self._reconnect_times) >= self._RECONNECT_WARN_THRESHOLD + # ------------------------------------------------------------------ # DataUpdateCoordinator hook # ------------------------------------------------------------------ @@ -470,7 +500,13 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): self._consecutive_poll_timeouts = 0 # One reconnect attempt — pause briefly so the device can # clean up its DTLS session state before we knock again. - self._log.warning("poll failed, reconnecting: %s", e) + # A lone reconnect is routine (see the README's "Known + # device behavior" section); only warn once they're piling + # up within the trailing window. + if self._reconnect_is_frequent(): + self._log.warning("poll failed, reconnecting: %s", e) + else: + self._log.info("poll failed, reconnecting: %s", e) await self.hass.async_add_executor_job(self._close_session) await asyncio.sleep(self._RECONNECT_PAUSE_S) try: diff --git a/custom_components/localthings/fan.py b/custom_components/localthings/fan.py index f21905f..15ac111 100644 --- a/custom_components/localthings/fan.py +++ b/custom_components/localthings/fan.py @@ -1,7 +1,15 @@ -"""Fan platform for Samsung range hoods.""" +"""Fan platform for Samsung range hoods and air purifiers. + +Two FanDesc-bound hrefs exist, one per family, dispatched by href in +async_setup_entry below since they need different HA fan semantics: the +range hood's fan speed is an ordered set of numeric levels (SET_SPEED), while +the newer air-purifier board family's modes (Smart/Max/Mid/WindFree/Sleep, +issue #130) are named behaviors with no linear order (PRESET_MODE).""" from __future__ import annotations +import logging + from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -14,14 +22,19 @@ from homeassistant.util.percentage import ( from .const import DOMAIN from .coordinator import LocalThingsCoordinator from .entity import LocalThingsEntity, _is_included +from .registry.capabilities.air_purifier import HREF_MODE as AIR_PURIFIER_FAN_HREF from .registry.entities import FanDesc +_LOGGER = logging.getLogger(__name__) POWER_HREF = '/power/0' POWER_VS_HREF = '/power/vs/0' _FAN_SPEED_FIELD = 'x.com.samsung.da.hood.fanSpeed' _SUPPORTED_FAN_SPEED_FIELD = 'x.com.samsung.da.hood.supportedFanSpeed' +_MODES_FIELD = 'x.com.samsung.da.modes' +_SUPPORTED_MODES_FIELD = 'x.com.samsung.da.supportedModes' + async def async_setup_entry( hass: HomeAssistant, @@ -29,11 +42,15 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: coordinator: LocalThingsCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - LocalThingsRangeHoodFan(coordinator, bound) - for bound in coordinator.bound - if isinstance(bound.desc, FanDesc) and _is_included(bound, coordinator) - ) + entities = [] + for bound in coordinator.bound: + if not (isinstance(bound.desc, FanDesc) and _is_included(bound, coordinator)): + continue + if bound.href == AIR_PURIFIER_FAN_HREF: + entities.append(LocalThingsAirPurifierFan(coordinator, bound)) + else: + entities.append(LocalThingsRangeHoodFan(coordinator, bound)) + async_add_entities(entities) class LocalThingsRangeHoodFan(LocalThingsEntity, FanEntity): @@ -120,3 +137,80 @@ class LocalThingsRangeHoodFan(LocalThingsEntity, FanEntity): ) code = percentage_to_ordered_list_item(codes, percentage) await self.coordinator.async_send_command(self._bound, ('speed', code)) + + +class LocalThingsAirPurifierFan(LocalThingsEntity, FanEntity): + """Air-purifier fan: named preset modes, not an ordered percentage -- + see air_purifier.FAN's comment for why (Smart/WindFree/Sleep aren't + "faster/slower" than Max/Mid).""" + + _enable_turn_on_off_backwards_compatibility = False + _attr_supported_features = ( + FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_ON + | FanEntityFeature.TURN_OFF + ) + + def __init__(self, coordinator: LocalThingsCoordinator, bound) -> None: + super().__init__(coordinator, bound) + self._attr_name = None + + def _rep(self, href: str) -> dict: + return self.coordinator.resource(href) or {} + + def _mode_rep(self) -> dict: + return self._rep(self._bound.href) + + def _power_payload(self, enabled: bool) -> tuple[str, bool, str]: + """Target whichever power resource this unit actually exposes -- + same pattern as LocalThingsRangeHoodFan._power_payload above. + Writing a hardcoded href here would silently no-op on a board that + only reports the other one, even though is_on already falls back + correctly.""" + resources = self.coordinator.last_resources + target = POWER_VS_HREF if POWER_VS_HREF in resources else POWER_HREF + return 'power', enabled, target + + @property + def is_on(self) -> bool: + power = self._rep(POWER_VS_HREF).get('x.com.samsung.da.power') + if power is not None: + return str(power).lower() == 'on' + return bool(self._rep(POWER_HREF).get('value')) + + @property + def preset_modes(self) -> list[str]: + return [ + str(code).lower() + for code in self._mode_rep().get(_SUPPORTED_MODES_FIELD, ()) + ] + + @property + def preset_mode(self) -> str | None: + modes = self._mode_rep().get(_MODES_FIELD) + code = modes[0] if isinstance(modes, (list, tuple)) and modes else modes + return str(code).lower() if code is not None else None + + async def async_turn_on( + self, percentage: int | None = None, preset_mode: str | None = None, + **kwargs, + ) -> None: + await self.coordinator.async_send_command(self._bound, self._power_payload(True)) + if preset_mode is not None: + await self.async_set_preset_mode(preset_mode) + + async def async_turn_off(self, **kwargs) -> None: + await self.coordinator.async_send_command(self._bound, self._power_payload(False)) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + # Reverse-resolve against the unit's own supportedModes -- the + # write needs the raw device code (e.g. 'WindFree'), not the + # lowercased HA value. + for code in self._mode_rep().get(_SUPPORTED_MODES_FIELD, ()): + if str(code).lower() == preset_mode: + await self.coordinator.async_send_command(self._bound, ('mode', code)) + return + _LOGGER.warning( + "%s: %r is not a valid preset mode (supported: %s)", + self.entity_id, preset_mode, self.preset_modes, + ) diff --git a/custom_components/localthings/registry/by_type/__init__.py b/custom_components/localthings/registry/by_type/__init__.py index 712dace..13107d8 100644 --- a/custom_components/localthings/registry/by_type/__init__.py +++ b/custom_components/localthings/registry/by_type/__init__.py @@ -5,7 +5,7 @@ from ._base import DeviceRegistry from . import ( air_purifier, airconditioner, cooktop, dehumidifier, dishwasher, dryer, induction_cooktop, oven, range as _range, range_hood, refrigerator, - washer, water_purifier, + vacuum_station, washer, water_purifier, ) __all__ = [ @@ -29,6 +29,7 @@ _REGISTRY_BY_KEY: dict[str, DeviceRegistry] = { 'range': _range.REGISTRY, 'range_hood': range_hood.REGISTRY, 'refrigerator': refrigerator.REGISTRY, + 'vacuum_station': vacuum_station.REGISTRY, 'washer': washer.REGISTRY, 'water_purifier': water_purifier.REGISTRY, } @@ -198,6 +199,16 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi # here) -- no WAC-specific resources needed. if key is None and '_WAC_' in (model_num or ''): key = 'airconditioner' + # ARA-WW-class wall-mount RACs (e.g. ARA-WW-TP1-22-COMMON, issues #115/ + # #116/#117/#120) report no oneUiVersion and no '_RAC_'/'-RAC-' token at + # all -- the board family is spelled 'ARA-WW-' instead. Same resource + # surface as the other TP1X-class room ACs above (mode/convenient/wind/ + # temperature/power/filter/humidity all confirmed against these dumps + # binding cleanly against the existing airconditioner registry once + # routed here) -- no ARA-WW-specific resources needed, so this reuses + # the same registry rather than adding a new device type. + if key is None and 'ARA-WW-' in (model_num or '').upper(): + key = 'airconditioner' # Air purifiers (e.g. ARTIK051_TVTL_18K, issue #56) report no # oneUiVersion either, and carry the '_TVTL_' board-family token. if key is None and '_TVTL_' in (model_num or ''): @@ -221,6 +232,15 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi # oneUiVersion and doesn't match the washer/dryer/dishwasher prefix map. if key is None and '-OVEN-' in (model_num or '').upper(): key = 'oven' + # Combi microwaves (e.g. TP1X_DA-KS-MICROWAVE-01041, issue #121) -- same + # board family as the wall oven above (an '/oven/vs/0' cavity resource + # with Convection/AirFryer/Grill/MicroWave modes on /mode/vs/0), just a + # different cavity. Reports no oneUiVersion and doesn't match the + # washer/dryer/dishwasher prefix map either. Binds cleanly against the + # existing oven registry once routed here (confirmed against the issue + # #121 dump) -- no microwave-specific device type needed. + if key is None and '-MICROWAVE-' in (model_num or '').upper(): + key = 'oven' # Standalone induction cooktops (e.g. TP1X_DA-KS-COOKTOP-01011, issue # #86) -- same board family and '/cooktop/status/vs/0' resource shape # as the range combo above, but no oven attached at all. Distinct from @@ -231,6 +251,13 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi # board family's. if key is None and '-COOKTOP-' in (model_num or '').upper(): key = 'induction_cooktop' + # Stick-vacuum clean/auto-empty station (e.g. A-VSKR-TP1-22-VS9500AL, + # issue #131) -- reports no oneUiVersion and carries the '-VSKR-' + # board-family token. See capabilities/vacuum_station.py for why this + # is its own device type: the resource set (dustbag/dustbin/UV-sanitize + # station state) shares no hrefs with anything else already modeled. + if key is None and '-VSKR-' in (model_num or '').upper(): + key = 'vacuum_station' # Consumer-model prefix from `description` (washer/dryer/dishwasher) -- # last, since it's the fuzziest match (a bare 2-letter prefix) and would # otherwise shadow the more specific board-family tokens above. diff --git a/custom_components/localthings/registry/by_type/air_purifier.py b/custom_components/localthings/registry/by_type/air_purifier.py index 1042455..2648aec 100644 --- a/custom_components/localthings/registry/by_type/air_purifier.py +++ b/custom_components/localthings/registry/by_type/air_purifier.py @@ -1,10 +1,22 @@ -"""Air-purifier device registry (Samsung ARTIK051_TVTL-class, issue #56). +"""Air-purifier device registry. -Reports no oneUiVersion; resolved via for_device_by_model's '_TVTL_' modelNum -token (see registry.py). Reuses dishwasher.DIAGNOSIS for /diagnosis/vs/0 -(identical field/write contract). +Spans two board generations sharing this one registry (see +capabilities/air_purifier.py's module docstring for the per-href +match_fn discriminators that keep them from colliding): + +- ARTIK051_TVTL-class (issue #56). Reports no oneUiVersion; resolved via + for_device_by_model's '_TVTL_' modelNum token (see registry.py). +- TP1X_DA-AC-AIR-class (issue #130). Self-reports oneUiVersion "7.0 Air + purifier", resolved via for_device(). Adds real fan-mode control plus + display/HEPA-filter/pet-filter/sound resources the older family never + reported; reuses airconditioner.DISPLAY_LIGHT and airconditioner.MUTE_ONCE + for /light/vs/0 and /option/muteonce/vs/0, which are identical shapes on + the shared DA-AC- board family. + +Reuses dishwasher.DIAGNOSIS for /diagnosis/vs/0 (identical field/write +contract). """ -from ..capabilities import air_purifier, common, dishwasher, ignored +from ..capabilities import air_purifier, airconditioner, common, dishwasher, ignored from ._base import DeviceRegistry, _build REGISTRY = DeviceRegistry( @@ -20,6 +32,16 @@ REGISTRY = DeviceRegistry( air_purifier.AIRFLOW_GENERIC, air_purifier.AIRFLOW_VS_FALLBACK, air_purifier.MODE, + air_purifier.FAN, + air_purifier.DISPLAY, + air_purifier.HEPA_FILTER, + air_purifier.PANEL_STATUS, + air_purifier.PET_FILTER_ACTIVATION, + air_purifier.SOUND_MODE, + air_purifier.SOUND_OUTPUT, + air_purifier.SOUND_VOLUME, + airconditioner.DISPLAY_LIGHT, + airconditioner.MUTE_ONCE, *air_purifier.COVERAGE, ]), ) diff --git a/custom_components/localthings/registry/by_type/airconditioner.py b/custom_components/localthings/registry/by_type/airconditioner.py index 7a7c38c..9ea273a 100644 --- a/custom_components/localthings/registry/by_type/airconditioner.py +++ b/custom_components/localthings/registry/by_type/airconditioner.py @@ -25,6 +25,7 @@ REGISTRY = DeviceRegistry( airconditioner.DISPLAY_LIGHT, airconditioner.MUTE_ONCE, airconditioner.CURRENT_LIMIT, + airconditioner.ANOMALY_LOAD, airconditioner.CURRENT_TEMPERATURE, airconditioner.CURRENT_TEMPERATURE_VS, airconditioner.HUMIDITY, diff --git a/custom_components/localthings/registry/by_type/oven.py b/custom_components/localthings/registry/by_type/oven.py index c4028ce..252f1dc 100644 --- a/custom_components/localthings/registry/by_type/oven.py +++ b/custom_components/localthings/registry/by_type/oven.py @@ -15,5 +15,6 @@ REGISTRY = DeviceRegistry( oven.OVEN_DOOR, oven.OVEN_CONNECTED, oven.OVEN_SPEC, + oven.OVEN_RECIPE_COOK, ]), ) diff --git a/custom_components/localthings/registry/by_type/vacuum_station.py b/custom_components/localthings/registry/by_type/vacuum_station.py new file mode 100644 index 0000000..80c1f3f --- /dev/null +++ b/custom_components/localthings/registry/by_type/vacuum_station.py @@ -0,0 +1,22 @@ +"""Stick-vacuum clean/auto-empty station device registry (issue #131). + +See capabilities/vacuum_station.py's module docstring for why this only +covers the station's own dustbag/dustbin/UV-sanitize state and not any +vacuum-body control (suction, battery, cleaning mode) -- the diagnostics +dump this was built from reports none of that. +""" +from ..capabilities import common, ignored, vacuum_station +from ._base import DeviceRegistry, _build + +REGISTRY = DeviceRegistry( + name='vacuum_station', + capabilities=_build([ + *ignored.IGNORED, + *common.UNIVERSAL, + *common.POWER, + vacuum_station.DUSTBAG, + vacuum_station.DUSTBAG_USAGE, + vacuum_station.DUSTBIN_SETTING, + vacuum_station.CLEANSTATION_STATUS, + ]), +) diff --git a/custom_components/localthings/registry/capabilities/air_purifier.py b/custom_components/localthings/registry/capabilities/air_purifier.py index 136d524..5e867d6 100644 --- a/custom_components/localthings/registry/capabilities/air_purifier.py +++ b/custom_components/localthings/registry/capabilities/air_purifier.py @@ -49,10 +49,26 @@ diagnostics snapshot was taken. Exposed read-only pending a confirmed, stable capture -- see the issue #56 discussion for what's needed. """ from ..capability import Capability -from ..entities import BinarySensorDesc, SensorDesc, SwitchDesc -from .common import int_or_none, sensor_item_value +from ..entities import ( + BinarySensorDesc, FanDesc, NumberDesc, SelectDesc, SensorDesc, SwitchDesc, +) +from .common import filter_usage_percent, int_or_none, sensor_item_value from .laundry import bool_option_exists, bool_option_value, option_value, option_write +# Newer TP1X_DA-AC-AIR-class boards (e.g. TP1X_DA-AC-AIR-01031_0000, issue +# #130) report fan modes directly on /mode/vs/0's top-level `modes`/ +# `supportedModes` fields (Smart/Max/Mid/WindFree/Sleep) instead of packing +# everything into the options[] array the way the older ARTIK051_TVTL +# family above does -- that older family's /mode/vs/0 has no top-level +# supportedModes at all (see the module docstring's Comode_Off finding). +# Both board generations share the /mode/vs/0 href, so FAN and MODE below +# are mutually exclusive via this presence check rather than colliding. +HREF_MODE = '/mode/vs/0' + + +def _has_top_level_modes(rep, resources): + return isinstance(rep.get('x.com.samsung.da.supportedModes'), (list, tuple)) + _AIR_QUALITY_SENSORS = ( ('dust', 'mdi:blur', 'Dust'), ('fine_dust', 'mdi:blur', 'FineDust'), @@ -156,6 +172,7 @@ def _light_write(payload, rep, href=None): MODE = Capability( href='/mode/vs/0', poll_tier='warm', + match_fn=lambda rep, resources: not _has_top_level_modes(rep, resources), entities=( SwitchDesc(key='display_light', icon='mdi:led-on', entity_category='config', @@ -171,11 +188,200 @@ MODE = Capability( ), ) + +def _fan_write(payload, rep, href=None): + kind, value, *args = payload + if kind == 'power': + # Targets whichever power href fan.py's _power_payload picked (the + # board may only report /power/0) -- a hardcoded vendor href here + # would silently no-op on such a board even though the entity's + # own is_on already falls back to reading it correctly. + power_href = args[0] if args else '/power/vs/0' + if power_href == '/power/0': + return ['power', '0'], {'value': bool(value)} + return (['power', 'vs', '0'], + {'x.com.samsung.da.power': 'On' if value else 'Off'}) + if kind == 'mode': + return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': [value]} + return None + + +def _first_fan_mode(rep): + """Representative scalar for the fan entity in the flattened state + (golden/regression), mirroring airconditioner.py's own _first_mode -- + the real entity computes its state from live coordinator reads.""" + modes = rep.get('x.com.samsung.da.modes') + if isinstance(modes, (list, tuple)): + return modes[0] if modes else None + return modes + + +# Named preset modes (Smart/Max/Mid/WindFree/Sleep), not an ordered +# percentage -- WindFree/Smart/Sleep are named behaviors, not +# "faster/slower" positions relative to Max/Mid, so fan.py's entity for +# this only exposes PRESET_MODE, matching how the AC family's own named +# convenient modes are modeled as a preset rather than a speed number. +FAN = Capability( + href=HREF_MODE, + poll_tier='warm', + match_fn=_has_top_level_modes, + entities=( + FanDesc(key='fan', translation_key='air_purifier_fan', + rep_fn=_first_fan_mode, write_fn=_fan_write), + ), +) + +# --------------------------------------------------------------------------- +# TP1X_DA-AC-AIR-class additions (issue #130). This board reports several +# resources the older ARTIK051_TVTL family never did. +# --------------------------------------------------------------------------- + +# Screen/indicator-panel on/off -- distinct from LIGHT below (ambient mood +# light): both report the same {mode, supportedModes: [On, Off]} shape on +# separate hrefs on this dump, so they're two independent physical controls, +# not a duplicate encoding of one. +DISPLAY = Capability( + href='/display/vs/0', + poll_tier='cold', + entities=( + SwitchDesc(key='display', field='mode', + icon='mdi:monitor', + entity_category='config', + value_fn=lambda v: v == 'On', + write_fn=lambda p, rep, href=None: ( + ['display', 'vs', '0'], + {'mode': 'On' if p == 'On' else 'Off'})), + ), +) + +# Same filterUsage/filterCapacity/filterStatus shape as the AC family's own +# AIR_FILTER (airconditioner.py) -- confirmed normal/wash/replace values not +# seen on this one dump, so the option list there is reused as-is rather +# than re-deriving it from a single sample. +HEPA_FILTER = Capability( + href='/filter/hepafilter/vs/0', + poll_tier='cold', + entities=( + SensorDesc(key='hepa_filter_usage', rep_fn=filter_usage_percent, + unit='%', state_class='measurement', + icon='mdi:air-filter', entity_category='diagnostic'), + SensorDesc(key='hepa_filter_status', field='x.com.samsung.da.filterStatus', + device_class='enum', + options=('normal', 'wash', 'replace'), + translation_key='filter_status', + icon='mdi:air-filter', entity_category='diagnostic', + value_fn=lambda v: v.lower() if isinstance(v, str) else v), + ), +) + +# Physical panel/cover status -- meaning of the one value seen ('Close') is +# plausible (the HEPA-filter access cover) but unconfirmed, and no +# supportedStatus list is present to check against -- exposed as a plain +# diagnostic sensor rather than an asserted binary_sensor polarity. +PANEL_STATUS = Capability( + href='/panel/vs/0', + poll_tier='cold', + entities=( + SensorDesc(key='panel_status', field='status', + icon='mdi:archive-outline', entity_category='diagnostic'), + ), +) + +# Pet-care filter mode -- a plain On/Off field with no vendor prefix, same +# convention as airconditioner.MUTE_ONCE. +PET_FILTER_ACTIVATION = Capability( + href='/petfilteractivation/vs/0', + poll_tier='cold', + entities=( + SwitchDesc(key='pet_filter_activation', field='status', + icon='mdi:paw', + entity_category='config', + value_fn=lambda v: v == 'On', + write_fn=lambda p, rep, href=None: ( + ['petfilteractivation', 'vs', '0'], + {'status': 'On' if p == 'On' else 'Off'})), + ), +) + +# Sound mode/volume shapes look like laundry.py's SOUND_MODE/SOUND_VOLUME at +# a glance, but this board's actual values differ (supportedModes here is +# ['mute', 'buzzer'], not laundry's hardcoded voice/tone/mute; volume range +# is 0-3, not laundry's fixed 0-15) -- reusing those would either reject a +# valid write ('buzzer') or expose the wrong number range, so these are +# separate descriptors reading the live supported values instead of a +# hardcoded table. +SOUND_MODE = Capability( + href='/settings/sound/mode/vs/0', + poll_tier='cold', + entities=( + # Distinct translation_key from laundry.SOUND_MODE's shared + # 'sound_mode' catalog entry -- that one's state table is + # {voice, tone, mute}, but this board's supportedModes is + # {mute, buzzer}. Sharing the key would leave 'buzzer' unlabelled + # (falls through to the raw code) since the catalogs don't overlap. + SelectDesc(key='sound_mode', translation_key='air_purifier_sound_mode', + field='mode', + icon='mdi:volume-high', + entity_category='config', + options_field='supportedModes', + write_fn=lambda p, rep, href=None: ( + ['settings', 'sound', 'mode', 'vs', '0'], {'mode': p})), + ), +) + +# Read-only descriptor of which sound output the unit has -- only one value +# seen, no alternatives to select between. +SOUND_OUTPUT = Capability( + href='/settings/sound/output/vs/0', + poll_tier='cold', + entities=( + SensorDesc(key='sound_output', field='deviceType', + icon='mdi:volume-high', entity_category='diagnostic'), + ), +) + +SOUND_VOLUME = Capability( + href='/settings/sound/volume/vs/0', + poll_tier='cold', + entities=( + NumberDesc(key='sound_volume', field='level', + icon='mdi:volume-medium', + entity_category='config', + native_min_fn=lambda rep: int_or_none(rep.get('minLevel')) or 0, + native_max_fn=lambda rep: int_or_none(rep.get('maxLevel')) or 0, + step_fn=lambda rep: int_or_none(rep.get('resolution')) or 1, + value_fn=int_or_none, + write_fn=lambda p, rep, href=None: ( + ['settings', 'sound', 'volume', 'vs', '0'], + {'level': str(int(p))})), + ), +) + # /humidity/0 and /humidity/vs/0 are empty {} on both dumps this family has # been verified against -- covered here (not globally, per ignored.py's # module docstring) since those hrefs collide with fridge/AC schemas # elsewhere. Same two hrefs and reasoning as airconditioner.py's _AC_IGNORED. +# +# The next six hrefs (issue #130, TP1X_DA-AC-AIR board) are the exact same +# resources, same shapes, same reasoning as airconditioner.py's +# _AC_IGNORED on the shared DA-AC- board family -- duplicated here rather +# than promoted to the global ignored.py list, since that would require +# also removing them from _AC_IGNORED in the same change (a global entry +# colliding with a family-local bare Capability on the same href raises in +# _build()); left as a possible follow-up DRY cleanup. COVERAGE = [ Capability(href='/humidity/0'), Capability(href='/humidity/vs/0'), + Capability(href='/airlevelcheck/vs/0'), # periodic air-quality sensing scheduler plumbing + Capability(href='/availablecontrolsets/vs/0'), # opaque hex-encoded control-set bitmap + Capability(href='/da/softreset/vs/0'), # soft-reset trigger plumbing + Capability(href='/keepnormalstate/vs/0'), # internal keep-normal flag + Capability(href='/personality/presence/vs/0'), # presence-personalization plumbing (empty here) + Capability(href='/reserverulesets/vs/0'), # opaque hex-encoded schedule reservation blob + # Do-not-disturb/auto-sleep schedule (visible/startTime/endTime/ + # useTimeSetting/functionState) -- every field reads its inert default + # on the only dump seen (times both '00:00:00', useTimeSetting/ + # functionState both 'false'). Same "needs a multi-field schedule + # editor" treatment as fridge.py's /defrost/reservation/vs/0. + Capability(href='/dnd/autosleep/vs/0'), ] diff --git a/custom_components/localthings/registry/capabilities/airconditioner.py b/custom_components/localthings/registry/capabilities/airconditioner.py index 67bec22..91f0466 100644 --- a/custom_components/localthings/registry/capabilities/airconditioner.py +++ b/custom_components/localthings/registry/capabilities/airconditioner.py @@ -16,7 +16,7 @@ by_type registry. """ from ..capability import Capability from ..entities import BinarySensorDesc, ClimateDesc, SensorDesc, SwitchDesc -from .common import normalize_temp_unit +from .common import filter_usage_percent, normalize_temp_unit from .laundry import option_write # --------------------------------------------------------------------------- @@ -35,13 +35,19 @@ HREF_TEMP_DESIRED = '/temperature/desired/0' # target_temperature (write ta HREF_TEMP_CONTROL = '/temperature/control/vs/0' # target_temperature_step HREF_WIND_STRENGTH = '/wind/strength/vs/0' # fan_mode HREF_WIND_DIRECTION = '/wind/direction/vs/0' # swing_mode +# Newer boards (issue #126, TP1X_DA-AC-RAC-01011 WindFree variant) report no +# HREF_WIND_DIRECTION at all and instead carry a 2-axis oscillation resource +# (separate vertical/horizontal Swing|Fix toggles, each with its own angle). +# climate.py falls back to this when HREF_WIND_DIRECTION is absent, the same +# duality pattern as the OCF/vendor temperature channel below. +HREF_WIND_OSCILLATION = '/wind/oscillation/vs/0' # swing_mode fallback HREF_CONVENIENT = '/mode/convenient/vs/0' # preset_mode HREF_TEMPS_VS = '/temperatures/vs/0' # vendor temp fallback (items[] array) CLIMATE_CONSUMED_HREFS = [ HREF_POWER, HREF_POWER_VS, HREF_TEMP_CURRENT, HREF_TEMP_DESIRED, HREF_TEMP_CONTROL, HREF_TEMPS_VS, HREF_WIND_STRENGTH, HREF_WIND_DIRECTION, - HREF_CONVENIENT, + HREF_WIND_OSCILLATION, HREF_CONVENIENT, ] @@ -71,18 +77,6 @@ def _temps_vs_unit(rep): return normalize_temp_unit(_temps_vs_item(rep).get('x.com.samsung.da.unit'), '°C') -def _filter_usage_percent(rep): - """Filter usage as a percentage of rated capacity. The device reports - `filterUsage` as a raw count in `filterCapacityUnit` (Hours here, e.g. - 100 of a 500 capacity), so a plain value with a '%' unit would be wrong -- - normalize to used/capacity. Returns None when capacity is missing/zero.""" - used = _num(rep.get('x.com.samsung.da.filterUsage')) - cap = _num(rep.get('x.com.samsung.da.filterCapacity')) - if used is None or not cap: - return None - return round(used / cap * 100) - - def _first_mode(rep): """Representative scalar for the climate entity in the flattened state (golden/regression). The real entity computes hvac_mode from power + mode.""" @@ -177,6 +171,15 @@ def _climate_write(payload, rep, href=None): return (['wind', 'strength', 'vs', '0'], {'x.com.samsung.da.modes': value}) if kind == 'swing': return (['wind', 'direction', 'vs', '0'], {'x.com.samsung.da.modes': value}) + if kind == 'oscillation': + # value is the HA swing_mode string ('off'/'vertical'/'horizontal'/ + # 'both'); both axes are independent Swing|Fix toggles on this + # resource, so one HA value maps to a pair of fields written + # together (see climate.py's oscillation fallback). + return (['wind', 'oscillation', 'vs', '0'], { + 'vertical': 'Swing' if value in ('vertical', 'both') else 'Fix', + 'horizontal': 'Swing' if value in ('horizontal', 'both') else 'Fix', + }) if kind == 'preset': return (['mode', 'convenient', 'vs', '0'], {'x.com.samsung.da.modes': value}) return None @@ -236,7 +239,7 @@ AIR_FILTER = Capability( href='/filter/airdustfilter/vs/0', poll_tier='cold', entities=( - SensorDesc(key='air_filter_usage', rep_fn=_filter_usage_percent, + SensorDesc(key='air_filter_usage', rep_fn=filter_usage_percent, unit='%', state_class='measurement', icon='mdi:air-filter', entity_category='diagnostic'), SensorDesc(key='air_filter_status', field='x.com.samsung.da.filterStatus', @@ -299,6 +302,31 @@ CURRENT_LIMIT = Capability( ), ) +# Overload-response setting (issue #126, TP1X_DA-AC-RAC-01011 WindFree +# variant): `operation` toggles the feature and `mode` picks 'Alarm' vs +# 'PowerSaving' out of `supportedModes`, with a `savingTime` duration +# ('20'/'40'/'60' minutes) alongside. Plausible shape (compressor-overload +# alarm vs. automatic power throttling), but nothing in the dump confirms +# the exact behavioral difference between the two modes or whether writing +# 'operation' is safe on live HVAC hardware -- exposed read-only per the +# 'don't guess' rule, same precedent as CURRENT_LIMIT above. +ANOMALY_LOAD = Capability( + href='/anomalyload/vs/0', + poll_tier='cold', + entities=( + BinarySensorDesc(key='overload_protection_active', field='operation', + icon='mdi:flash-alert', + entity_category='diagnostic', + value_fn=lambda v: v == 'On'), + SensorDesc(key='overload_protection_mode', field='mode', + device_class='enum', + options=('alarm', 'powersaving'), + translation_key='overload_protection_mode', + icon='mdi:flash-alert', entity_category='diagnostic', + value_fn=lambda v: v.lower() if isinstance(v, str) else v), + ), +) + # The climate entity already surfaces current_temperature as a card # attribute, but that's not enough for history graphs/automations/ # statistics -- issue #75 asked for a standalone sensor. Same OCF-standard- diff --git a/custom_components/localthings/registry/capabilities/common.py b/custom_components/localthings/registry/capabilities/common.py index 2b6dc59..cd03a91 100644 --- a/custom_components/localthings/registry/capabilities/common.py +++ b/custom_components/localthings/registry/capabilities/common.py @@ -10,6 +10,8 @@ against live device dumps: /water/consumption/vs/0 -> x.com.samsung.da.cumulativeWater /filter/waterfilter/vs/0 -> x.com.samsung.da.filterUsage / filterStatus """ +from datetime import datetime, timezone + from ..capability import Capability from ..entities import ( BinarySensorDesc, ButtonDesc, SelectDesc, SensorDesc, SwitchDesc, @@ -40,6 +42,35 @@ def wh_to_kwh(v): return round(n / 1000.0, 2) if n is not None else None +def parse_iso_utc(raw): + """ISO datetime defaulting to UTC when the string carries no timezone + of its own (this integration's convention for other bare ISO datetime + fields -- see washer.py's drum-clean-log comment). A few boards do + ship a 'Z'/offset suffix (fromisoformat parses that natively since + Python 3.11) -- only fill in UTC when parsing left the result naive, + rather than unconditionally overwriting whatever offset was parsed.""" + if not raw: + return None + try: + dt = datetime.fromisoformat(raw) + except ValueError: + return None + return dt if dt.tzinfo is not None else dt.replace(tzinfo=timezone.utc) + + +def filter_usage_percent(rep): + """Filter usage as a percentage of rated capacity. Several families + (AC, air purifier) report `filterUsage` as a raw count in + `filterCapacityUnit` (Hours, e.g. 100 of a 500 capacity), so a plain + value with a '%' unit would be wrong -- normalize to used/capacity. + Returns None when capacity is missing/zero.""" + used = _num(rep.get('x.com.samsung.da.filterUsage')) + cap = _num(rep.get('x.com.samsung.da.filterCapacity')) + if used is None or not cap: + return None + return round(used / cap * 100) + + def normalize_temp_unit(raw, default='°F'): """'C'/'Celsius' -> '°C', 'F'/'Fahrenheit' -> '°F'. Falls back to `default` for any other/missing value. Shared by fridge.py and oven.py, diff --git a/custom_components/localthings/registry/capabilities/oven.py b/custom_components/localthings/registry/capabilities/oven.py index c679fac..4b47a38 100644 --- a/custom_components/localthings/registry/capabilities/oven.py +++ b/custom_components/localthings/registry/capabilities/oven.py @@ -338,6 +338,12 @@ OVEN_CONNECTED = Capability( # fields worth exposing. OVEN_SPEC = Capability(href='/oven/spec/vs/0') +# Quick-recipe display blob (combi microwave, issue #121) -- a JSON-encoded +# string (language/menu/servingSize/option) with every field blank on the +# only dump seen, and no documented write contract. No entity to bind per +# the 'don't guess' rule; a bare Capability still marks the href covered. +OVEN_RECIPE_COOK = Capability(href='/recipe/cook/vs/0') + OVEN_MODE = Capability( href='/mode/vs/0', poll_tier='warm', diff --git a/custom_components/localthings/registry/capabilities/vacuum_station.py b/custom_components/localthings/registry/capabilities/vacuum_station.py new file mode 100644 index 0000000..090ed88 --- /dev/null +++ b/custom_components/localthings/registry/capabilities/vacuum_station.py @@ -0,0 +1,109 @@ +"""Capabilities for the Samsung stick-vacuum clean/auto-empty station +(model A-VSKR-TP1-22-VS9500AL, "Bespoke Jet" clean station, issue #131). + +The reporter's diagnostics dump shows no vacuum-body state at all (no +suction level, no battery, no cleaning-mode/room-mapping control, no +docked/undocked status even) -- only the clean station's own dustbag, +dustbin auto-empty settings, and UV-C sanitizing-cycle status. This +strongly suggests the WiFi/DTLS module lives in the station, not the +handheld stick: the station is the only "device" this integration's local +API can see, and the wand's own controls are unrelated hardware not +reachable this way. Modeled as its own device type -- these hrefs don't +overlap with any existing family, so there's no shared-href ambiguity to +resolve against another type (see registry/by_type/__init__.py's +docstring for that rule). + +Resources verified against the issue #131 diagnostics dump. +""" +from ..capability import Capability +from ..entities import BinarySensorDesc, SelectDesc, SensorDesc, SwitchDesc +from .common import int_or_none, parse_iso_utc as _parse_iso_utc + + +DUSTBAG = Capability( + href='/component/station/dustbag/vs/0', + poll_tier='warm', + entities=( + BinarySensorDesc(key='dustbag_full', field='x.com.samsung.da.status', + device_class='problem', + icon='mdi:bag-personal', + value_fn=lambda v: v == 'full'), + ), +) + +# dustbagUsage/dustbagPrevUsage are raw counters with no capacity/resolution +# field alongside them to normalize into a percentage (unlike the AC/range +# families' filterUsage, which always ships filterCapacity) -- exposed as a +# plain diagnostic count rather than guessing a unit. +DUSTBAG_USAGE = Capability( + href='/component/station/dustbagusage/vs/0', + poll_tier='cold', + entities=( + SensorDesc(key='dustbag_usage', field='x.com.samsung.da.dustbagUsage', + icon='mdi:counter', entity_category='diagnostic', + state_class='total_increasing', value_fn=int_or_none), + ), +) + +DUSTBIN_SETTING = Capability( + href='/setting/dustbin/vs/0', + poll_tier='warm', + entities=( + SwitchDesc(key='auto_empty', field='x.com.samsung.da.autoEmpty', + icon='mdi:delete-empty', + entity_category='config', + value_fn=lambda v: v == 'On', + write_fn=lambda p, rep, href=None: ( + ['setting', 'dustbin', 'vs', '0'], + {'x.com.samsung.da.autoEmpty': 'On' if p == 'On' else 'Off'})), + SwitchDesc(key='dustbin_auto_close', field='x.com.samsung.da.autoClose', + icon='mdi:door-sliding', + entity_category='config', + value_fn=lambda v: v == 'On', + write_fn=lambda p, rep, href=None: ( + ['setting', 'dustbin', 'vs', '0'], + {'x.com.samsung.da.autoClose': 'On' if p == 'On' else 'Off'})), + SelectDesc(key='discharging_time', field='x.com.samsung.da.desiredDischargingTime', + icon='mdi:timer-outline', + entity_category='config', + options_field='x.com.samsung.da.supportedDischargingTime', + write_fn=lambda p, rep, href=None: ( + ['setting', 'dustbin', 'vs', '0'], + {'x.com.samsung.da.desiredDischargingTime': p})), + ), +) + +# stickStatus's exact meaning (docked? powered? charging?) isn't confirmed +# from this single On/Off dump -- exposed as a plain diagnostic sensor +# rather than a binary_sensor, so no polarity/semantic is asserted that +# might be wrong. +CLEANSTATION_STATUS = Capability( + href='/status/cleanstation/vs/0', + poll_tier='warm', + entities=( + SensorDesc(key='cleanstation_status', field='x.com.samsung.da.status', + icon='mdi:home-lightning-bolt', entity_category='diagnostic'), + SensorDesc(key='stick_status', field='x.com.samsung.da.stickStatus', + icon='mdi:broom', entity_category='diagnostic'), + SwitchDesc(key='uvc_intensive_mode', field='x.com.samsung.da.uvcIntensive', + icon='mdi:lightbulb-on-outline', + entity_category='config', + value_fn=lambda v: v == 'On', + write_fn=lambda p, rep, href=None: ( + ['status', 'cleanstation', 'vs', '0'], + {'x.com.samsung.da.uvcIntensive': 'On' if p == 'On' else 'Off'})), + SensorDesc(key='uvc_operation_time', field='x.com.samsung.da.uvcOperationTime', + icon='mdi:timer-sand', entity_category='diagnostic', + value_fn=int_or_none), + SensorDesc(key='uvc_total_operation_time', + field='x.com.samsung.da.uvcTotalOperationTime', + icon='mdi:timer-sand', entity_category='diagnostic', + state_class='total_increasing', value_fn=int_or_none), + SensorDesc(key='uvc_finished_time', field='x.com.samsung.da.uvcFinishedTime', + device_class='timestamp', entity_category='diagnostic', + value_fn=_parse_iso_utc), + SensorDesc(key='uvc_emitted_time', field='x.com.samsung.da.emittedTime', + device_class='timestamp', entity_category='diagnostic', + value_fn=_parse_iso_utc), + ), +) diff --git a/custom_components/localthings/registry/capabilities/water_purifier.py b/custom_components/localthings/registry/capabilities/water_purifier.py index ef287cb..953cd3d 100644 --- a/custom_components/localthings/registry/capabilities/water_purifier.py +++ b/custom_components/localthings/registry/capabilities/water_purifier.py @@ -3,23 +3,9 @@ issue #90, model TP2X_WATERPURIFIER_20K). Resources verified against the issue #90 diagnostics dump. """ -from datetime import datetime, timezone - from ..capability import Capability from ..entities import BinarySensorDesc, NumberDesc, SelectDesc, SensorDesc, SwitchDesc -from .common import int_or_none - - -def _parse_iso_utc(raw): - """Bare ISO datetime with no timezone field alongside it -- treated as - UTC, matching this integration's convention for other bare ISO datetime - fields (see washer.py's drum-clean-log comment).""" - if not raw: - return None - try: - return datetime.fromisoformat(raw).replace(tzinfo=timezone.utc) - except ValueError: - return None +from .common import int_or_none, parse_iso_utc as _parse_iso_utc DISPENSE = Capability( diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index 1786d98..2b5c321 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -16,6 +16,9 @@ "cloud_connected": { "name": "Cloud connected" }, + "dustbag_full": { + "name": "Dust bag full" + }, "cooktop_power": { "name": "Cooktop power" }, @@ -25,6 +28,9 @@ "current_limit_enabled": { "name": "Current limit enabled" }, + "overload_protection_active": { + "name": "Overload protection active" + }, "cycle_active": { "name": "Running" }, @@ -128,6 +134,21 @@ } } }, + "fan": { + "air_purifier_fan": { + "state_attributes": { + "preset_mode": { + "state": { + "smart": "Smart", + "max": "Max", + "mid": "Mid", + "windfree": "WindFree", + "sleep": "Sleep" + } + } + } + } + }, "number": { "cook_time": { "name": "Cook time" @@ -185,6 +206,13 @@ "on": "On" } }, + "discharging_time": { + "name": "Discharging time", + "state": { + "1": "1 minute", + "3": "3 minutes" + } + }, "cycle": { "name": "Cycle" }, @@ -385,6 +413,13 @@ "mute": "Mute" } }, + "air_purifier_sound_mode": { + "name": "Sound mode", + "state": { + "mute": "Mute", + "buzzer": "Buzzer" + } + }, "spin_speed": { "name": "Spin speed", "state": { @@ -515,6 +550,43 @@ "current_limit_level": { "name": "Current limit level" }, + "hepa_filter_usage": { + "name": "HEPA filter usage" + }, + "panel_status": { + "name": "Panel status" + }, + "sound_output": { + "name": "Sound output" + }, + "dustbag_usage": { + "name": "Dust bag usage" + }, + "cleanstation_status": { + "name": "Clean station status" + }, + "stick_status": { + "name": "Stick status" + }, + "uvc_operation_time": { + "name": "UV-C operation time" + }, + "uvc_total_operation_time": { + "name": "UV-C total operation time" + }, + "uvc_finished_time": { + "name": "UV-C finished time" + }, + "uvc_emitted_time": { + "name": "UV-C emitted time" + }, + "overload_protection_mode": { + "name": "Overload protection mode", + "state": { + "alarm": "Alarm", + "powersaving": "Power saving" + } + }, "current_temp_c": { "name": "Temperature" }, @@ -729,6 +801,21 @@ "auto_clean": { "name": "Auto clean" }, + "display": { + "name": "Display" + }, + "pet_filter_activation": { + "name": "Pet filter activation" + }, + "auto_empty": { + "name": "Auto empty" + }, + "dustbin_auto_close": { + "name": "Dustbin auto-close" + }, + "uvc_intensive_mode": { + "name": "UV-C intensive mode" + }, "auto_door_opener": { "name": "Auto door opener" }, diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index a6e73ed..9699b1f 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -16,6 +16,9 @@ "cloud_connected": { "name": "Verbonden met cloud" }, + "dustbag_full": { + "name": "Stofzak vol" + }, "cooktop_power": { "name": "Kookplaat voeding" }, @@ -25,6 +28,9 @@ "current_limit_enabled": { "name": "Stroombegrenzing ingeschakeld" }, + "overload_protection_active": { + "name": "Overbelastingsbeveiliging actief" + }, "cycle_active": { "name": "Actief" }, @@ -128,6 +134,21 @@ } } }, + "fan": { + "air_purifier_fan": { + "state_attributes": { + "preset_mode": { + "state": { + "smart": "Slim", + "max": "Max", + "mid": "Gemiddeld", + "windfree": "WindFree", + "sleep": "Slaap" + } + } + } + } + }, "number": { "cook_time": { "name": "Bereidingstijd" @@ -185,6 +206,13 @@ "on": "Aan" } }, + "discharging_time": { + "name": "Leegtijd", + "state": { + "1": "1 minuut", + "3": "3 minuten" + } + }, "cycle": { "name": "Programma" }, @@ -385,6 +413,13 @@ "mute": "Dempen" } }, + "air_purifier_sound_mode": { + "name": "Geluidsmodus", + "state": { + "mute": "Dempen", + "buzzer": "Zoemer" + } + }, "spin_speed": { "name": "Centrifugesnelheid", "state": { @@ -515,6 +550,43 @@ "current_limit_level": { "name": "Niveau stroombegrenzing" }, + "hepa_filter_usage": { + "name": "HEPA-filtergebruik" + }, + "panel_status": { + "name": "Paneelstatus" + }, + "sound_output": { + "name": "Geluidsuitvoer" + }, + "dustbag_usage": { + "name": "Stofzakgebruik" + }, + "cleanstation_status": { + "name": "Status schoonmaakstation" + }, + "stick_status": { + "name": "Status steel" + }, + "uvc_operation_time": { + "name": "UV-C bedrijfstijd" + }, + "uvc_total_operation_time": { + "name": "UV-C totale bedrijfstijd" + }, + "uvc_finished_time": { + "name": "UV-C voltooid op" + }, + "uvc_emitted_time": { + "name": "UV-C uitgestraald op" + }, + "overload_protection_mode": { + "name": "Modus overbelastingsbeveiliging", + "state": { + "alarm": "Alarm", + "powersaving": "Energiebesparing" + } + }, "current_temp_c": { "name": "Temperatuur" }, @@ -729,6 +801,21 @@ "auto_clean": { "name": "Automatisch reinigen" }, + "display": { + "name": "Display" + }, + "pet_filter_activation": { + "name": "Huisdierfilter actief" + }, + "auto_empty": { + "name": "Automatisch legen" + }, + "dustbin_auto_close": { + "name": "Stofreservoir automatisch sluiten" + }, + "uvc_intensive_mode": { + "name": "Intensieve UV-C-modus" + }, "auto_door_opener": { "name": "Automatische deuropener" }, diff --git a/tests/fixtures/air_purifier_tp1x_da_ac_air_device.json b/tests/fixtures/air_purifier_tp1x_da_ac_air_device.json new file mode 100644 index 0000000..0098dc2 --- /dev/null +++ b/tests/fixtures/air_purifier_tp1x_da_ac_air_device.json @@ -0,0 +1,466 @@ +{ + "device0": [ + { + "rt": [ + "x.com.samsung.devcol", + "oic.wk.col" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ] + }, + { + "href": "/airlevelcheck/vs/0", + "rep": { + "x.com.samsung.da.sensingState": "NonProcessing", + "x.com.samsung.da.lastSensingTime": "1785157841", + "x.com.samsung.da.lastSensingLevel": "Kr1", + "x.com.samsung.da.periodicSensingSkipStatus": "Off", + "x.com.samsung.da.periodicSensingSkipTime": "00000000", + "x.com.samsung.da.periodicSensingActivationState": "On", + "x.com.samsung.da.autoExeState": "Off", + "x.com.samsung.da.supportedAutoExeState": [ + "Off", + "Airpurify", + "Alarm" + ], + "x.com.samsung.da.startSensingOnce": "Off" + } + }, + { + "href": "/alarms/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.alarmType": "Device", + "x.com.samsung.da.code": "ErrorCode_OFF", + "x.com.samsung.da.triggeredTime": "2026-07-27T13:18:51", + "x.com.samsung.da.state": "Deleted" + } + ], + "rt": [ + "x.com.samsung.da.alarms" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/availablecontrolsets/vs/0", + "rep": { + "x.com.samsung.da.sets": "3B000100000A0000000000000000", + "x.com.samsung.da.id": "VTL", + "x.com.samsung.da.version": "1.0" + } + }, + { + "href": "/configuration/vs/0", + "rep": { + "x.com.samsung.da.region": "2650000000", + "x.com.samsung.da.airconOptionList": [ + "AI_PURIFY", + "WELCOMECARE", + "DR" + ] + } + }, + { + "href": "/da/softreset/vs/0", + "rep": { + "x.com.samsung.da.softwarereset": "false" + } + }, + { + "href": "/devicespecificinfo/vs/0", + "rep": { + "x.com.samsung.da.deviceActive": true + } + }, + { + "href": "/display/vs/0", + "rep": { + "mode": "On", + "supportedModes": [ + "On", + "Off" + ] + } + }, + { + "href": "/dnd/autosleep/vs/0", + "rep": { + "x.com.samsung.da.visible": "true", + "x.com.samsung.da.startTime": "00:00:00", + "x.com.samsung.da.endTime": "00:00:00", + "x.com.samsung.da.useTimeSetting": "false", + "x.com.samsung.da.functionState": "false" + } + }, + { + "href": "/drlc/vs/0", + "rep": { + "x.com.samsung.da.start": "1970-01-01T00:00:00Z", + "x.com.samsung.da.override": "Off", + "x.com.samsung.da.durationminutes": "0", + "x.com.samsung.da.drlcLevel": "0", + "x.com.samsung.da.realSaving": "Off" + } + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.cumulativeDate": "1785158301", + "x.com.samsung.da.cumulativeSavedPower": "0", + "x.com.samsung.da.cumulativePower": "32428", + "x.com.samsung.da.cumulativeUnit": "Wh", + "x.com.samsung.da.instantaneousPowerUnit": "W", + "x.com.samsung.da.cumulativePowerType": "total" + } + }, + { + "href": "/file/information/vs/0", + "rep": { + "x.com.samsung.timeoffset": "+09:00", + "x.com.samsung.supprtedtype": 1 + } + }, + { + "href": "/filter/hepafilter/vs/0", + "rep": { + "x.com.samsung.da.filterUsage": "70", + "x.com.samsung.da.filterUsageResolution": "1", + "x.com.samsung.da.filterCapacity": "8760", + "x.com.samsung.da.filterCapacityUnit": "Hour", + "x.com.samsung.da.filterResetType": [ + "replaceable" + ], + "x.com.samsung.da.filterStatus": "normal" + } + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.otnDUID": "**REDACTED**", + "x.com.samsung.da.modelNum": "TP1X_DA-AC-AIR-01031_0000|10268241|70000721001813CC4D1A490301082100", + "x.com.samsung.da.description": "TP1X_DA-AC-AIR-01031_0000", + "x.com.samsung.da.serialNum": "**REDACTED**", + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Version", + "x.com.samsung.da.type": "Software", + "x.com.samsung.da.number": "02660A260401", + "x.com.samsung.da.newVersionAvailable": "0" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Version", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.newVersionAvailable": "0", + "x.com.samsung.da.number": "02682A25042102,02683A25031801" + } + ], + "x.com.samsung.da.diagDumpType": "file", + "x.com.samsung.da.diagEndPoint": "SSM", + "x.com.samsung.da.diagLogType": [ + "errCode", + "dump" + ], + "x.com.samsung.da.diagMnid": "0AJT", + "x.com.samsung.da.diagSetupid": "AP2", + "x.com.samsung.da.diagProtocolType": "BLE_OCF", + "x.com.samsung.da.diagMinVersion": "3.0", + "x.com.samsung.da.diagTsId": "DA01" + } + }, + { + "href": "/keepnormalstate/vs/0", + "rep": { + "x.com.samsung.da.keepnormal": 1 + } + }, + { + "href": "/kidslock/vs/0", + "rep": { + "x.com.samsung.da.kidsLock": "Ready" + } + }, + { + "href": "/light/vs/0", + "rep": { + "mode": "On", + "supportedModes": [ + "On", + "Off" + ] + } + }, + { + "href": "/mode/vs/0", + "rep": { + "x.com.samsung.da.modes": [ + "Smart" + ], + "x.com.samsung.da.supportedModes": [ + "Smart", + "Max", + "Mid", + "WindFree", + "Sleep" + ], + "x.com.samsung.da.options": [ + "OptionCode_23418", + "SmartSleep_Off", + "WelcomeCare_On", + "Pollution_Off", + "ProtectOperation_Off", + "UpdateAllow_NotAllowed" + ], + "rt": [ + "x.com.samsung.da.mode" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/option/muteonce/vs/0", + "rep": { + "muteonce": "Off" + } + }, + { + "href": "/panel/vs/0", + "rep": { + "status": "Close" + } + }, + { + "href": "/personality/presence/vs/0", + "rep": {} + }, + { + "href": "/petfilteractivation/vs/0", + "rep": { + "status": "Off" + } + }, + { + "href": "/power/vs/0", + "rep": { + "x.com.samsung.da.power": "On", + "operationNumber": "36", + "displayCondition": "Enable" + } + }, + { + "href": "/realtimenotiforclient/vs/0", + "rep": { + "x.com.samsung.da.timeforshortnoti": "0", + "x.com.samsung.da.longnotisubscription": "true", + "x.com.samsung.da.periodicnotisubscription": "true" + } + }, + { + "href": "/reserverulesets/vs/0", + "rep": { + "x.com.samsung.da.sets": "3F00000000000000000000", + "x.com.samsung.da.id": "VTL", + "x.com.samsung.da.version": "1.0" + } + }, + { + "href": "/sensors/vs/0", + "rep": { + "x.com.samsung.da.cleanLevel": "1", + "x.com.samsung.da.total": "5", + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Sensor for CleanLevel", + "x.com.samsung.da.type": "CleanLevel", + "x.com.samsung.da.value": [ + "1" + ] + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Sensor for Odor", + "x.com.samsung.da.type": "Odor", + "x.com.samsung.da.value": [ + "1" + ] + }, + { + "x.com.samsung.da.id": "2", + "x.com.samsung.da.description": "Sensor for Dust", + "x.com.samsung.da.type": "Dust", + "x.com.samsung.da.value": [ + "5", + "1" + ] + }, + { + "x.com.samsung.da.id": "3", + "x.com.samsung.da.description": "Sensor for FineDust", + "x.com.samsung.da.type": "FineDust", + "x.com.samsung.da.value": [ + "5", + "1" + ] + }, + { + "x.com.samsung.da.id": "4", + "x.com.samsung.da.description": "Sensor for SuperFineDust", + "x.com.samsung.da.type": "SuperFineDust", + "x.com.samsung.da.value": [ + "5", + "1" + ] + } + ], + "rt": [ + "x.com.samsung.da.sensors" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "href": "/settings/sound/mode/vs/0", + "rep": { + "mode": "mute", + "supportedModes": [ + "mute", + "buzzer" + ] + } + }, + { + "href": "/settings/sound/output/vs/0", + "rep": { + "deviceType": "buzzer" + } + }, + { + "href": "/settings/sound/volume/vs/0", + "rep": { + "level": "0", + "minLevel": "0", + "maxLevel": "3", + "resolution": "1" + } + }, + { + "href": "/otninformation/vs/0", + "rep": { + "x.com.samsung.da.target": "Micom", + "x.com.samsung.da.newVersionAvailable": "false", + "x.com.samsung.da.newVersionNo": "12250421", + "x.com.samsung.da.currentVersionInfo": "10000000", + "otnStatus": "None", + "flashingProgress": "0", + "otnTarget": "main", + "otnCompleteDate": "noHistory", + "scheduledTime": "None", + "swVersionInfo": { + "platform": "Tizen Lite", + "oneUiVersion": "7.0 Air purifier", + "osVersion": "4.0" + }, + "otnList": [ + { + "type": "WIFI", + "modelId": "AVT-KR-TP1-24-AXXX00", + "versions": [ + "12260401" + ], + "visVersion": "260401" + }, + { + "type": "Micom", + "modelId": "895010268241FFFFFFFF", + "versions": [ + "25042102", + "FFFFFFFF" + ], + "visVersion": "250421" + }, + { + "type": "Micom", + "modelId": "09501026824110268341", + "versions": [ + "25042102", + "25031801" + ], + "visVersion": "250421" + } + ] + } + }, + { + "href": "/timezone/vs/0", + "rep": { + "timezoneid": "Asia/Seoul", + "offset": "+09:00", + "DST": "OFF" + } + }, + { + "href": "/connectionconfig/vs/0", + "rep": { + "autoReconnectionMinVersion": "1.0", + "autoReconnection": "true", + "autoReconnectionProtocolType": [ + "helper_hotspot", + "ble_ocf" + ], + "supportedWiFiAuthType": [ + "OPEN", + "WEP", + "WPA-PSK", + "WPA2-PSK", + "SAE" + ], + "supportedWiFiCryptoType": [ + "TKIP", + "AES", + "WEP-64", + "WEP-128" + ], + "supportedWiFiFreq": [ + "2.4G" + ], + "calmConnectionCare": { + "version": "1.0", + "role": [ + "things" + ] + } + } + }, + { + "href": "/wirelessinfo/vs/0", + "rep": { + "macaddressWiFi": "**REDACTED**", + "macaddressBLE": "**REDACTED**", + "connectedApSsid": "Branden_Summit" + } + }, + { + "href": "/quickcontrol/info/vs/0", + "rep": { + "supportedVersion": "1.0" + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/airconditioner_ara_ww_tp1_22_device.json b/tests/fixtures/airconditioner_ara_ww_tp1_22_device.json new file mode 100644 index 0000000..d396db9 --- /dev/null +++ b/tests/fixtures/airconditioner_ara_ww_tp1_22_device.json @@ -0,0 +1,476 @@ +{ + "device0": [ + { + "rt": [ + "x.com.samsung.devcol", + "oic.wk.col" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ] + }, + { + "href": "/personality/presence/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "", + "x.com.samsung.da.deviceId": "**REDACTED**", + "x.com.samsung.da.value": "" + } + ] + } + }, + { + "href": "/realtimenotiforclient/vs/0", + "rep": { + "x.com.samsung.da.timeforshortnoti": "0", + "x.com.samsung.da.longnotisubscription": "false", + "x.com.samsung.da.periodicnotisubscription": "true" + } + }, + { + "href": "/filter/airdustfilter/vs/0", + "rep": { + "x.com.samsung.da.filterUsage": "54", + "x.com.samsung.da.filterUsageResolution": "1", + "x.com.samsung.da.filterDesiredUsage": "500", + "x.com.samsung.da.filterStatus": "normal", + "x.com.samsung.da.filterCapacity": "500", + "x.com.samsung.da.filterCapacityUnit": "Hour", + "x.com.samsung.da.filterResetType": [ + "replaceable", + "washable" + ] + } + }, + { + "href": "/temperature/control/vs/0", + "rep": { + "x.com.samsung.da.increment": "1" + } + }, + { + "href": "/mode/convenient/vs/0", + "rep": { + "x.com.samsung.da.modes": "Off", + "x.com.samsung.da.supportedModes": [ + "Off", + "Sleep", + "Quiet", + "Smart", + "Speed", + "Nano", + "NanoSleep" + ] + } + }, + { + "href": "/option/autoclean/vs/0", + "rep": { + "x.com.samsung.da.status": "Stop", + "x.com.samsung.da.settingStatus": "Off", + "x.com.samsung.da.progress": "0", + "x.com.samsung.da.supportedStatus": [ + "Start", + "Stop" + ], + "x.com.samsung.da.supportedSettingStatus": [ + "On", + "Off" + ] + } + }, + { + "href": "/wind/strength/vs/0", + "rep": { + "x.com.samsung.da.modes": "0", + "x.com.samsung.da.supportedModes": [ + "0", + "1", + "2", + "3", + "4" + ], + "x.com.samsung.da.modesName": [ + "Auto", + "Low", + "Mid", + "High", + "Turbo" + ] + } + }, + { + "href": "/wind/direction/vs/0", + "rep": { + "x.com.samsung.da.modes": "Left_And_Right", + "x.com.samsung.da.supportedModes": [ + "Fix", + "Up_And_Low", + "Left_And_Right", + "All" + ] + } + }, + { + "href": "/alarms/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Alarm", + "x.com.samsung.da.alarmType": "Device", + "x.com.samsung.da.code": "ErrorCode_OFF", + "x.com.samsung.da.triggeredTime": "2026-07-27T06:07:25", + "x.com.samsung.da.state": "Deleted" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Alarm", + "x.com.samsung.da.alarmType": "Device", + "x.com.samsung.da.code": "FilterAlarm_OFF", + "x.com.samsung.da.triggeredTime": "2026-07-27T06:07:25", + "x.com.samsung.da.state": "Deleted" + } + ] + } + }, + { + "href": "/temperatures/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Temperature", + "x.com.samsung.da.desired": "26.0", + "x.com.samsung.da.current": "30.0", + "x.com.samsung.da.maximum": "30", + "x.com.samsung.da.minimum": "18", + "x.com.samsung.da.increment": "1.0", + "x.com.samsung.da.unit": "Celsius" + } + ] + } + }, + { + "href": "/temperature/current/0", + "rep": { + "range": [ + 16, + 30 + ], + "units": "C", + "temperature": 30.0 + } + }, + { + "href": "/temperature/desired/0", + "rep": { + "range": [ + 16, + 30 + ], + "units": "C", + "temperature": 26.0 + } + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.instantaneousPower": "0.000000", + "x.com.samsung.da.cumulativePower": "3193591", + "x.com.samsung.da.cumulativeSavedPower": "158331", + "x.com.samsung.da.cumulativeUnit": "Wh", + "x.com.samsung.da.instantaneousPowerUnit": "W" + } + }, + { + "href": "/energy/consumption/0", + "rep": { + "power": 0.0 + } + }, + { + "href": "/mode/vs/0", + "rep": { + "x.com.samsung.da.supportedModes": [ + "Auto", + "Cool", + "Dry", + "Wind" + ], + "x.com.samsung.da.modes": [ + "Dry" + ], + "x.com.samsung.da.options": [ + "Sleep_0", + "ArtificialWorking_Off", + "ComfortAICooling_Off", + "AiTempChanged_Off", + "AiTemp_270", + "OutdoorConnection_Connected", + "OutdoorTemp_88", + "CoolCapa_53", + "WarmCapa_0", + "Light_Off", + "Volume_100", + "StopAutoClean_Idle", + "Autoclean_Off", + "DiagnosisAI_Off", + "ProgressDiagnosisAI_1", + "ResultDiagnosisAI_Normal", + "SmartCoolClean_Off", + "ProgressSmartClean_0", + "FreezeAlarmSetting_On", + "DesiredFreezeAlarm_112", + "WashAlarm_Off", + "OptionCode_56440", + "ExtendOptionCode_246413", + "RacInfo_None", + "UpdateAllow_NotAllowed", + "DurationOn_0", + "WelcomeCoolingState_On" + ] + } + }, + { + "href": "/power/vs/0", + "rep": { + "x.com.samsung.da.power": "Off", + "x.com.samsung.da.displaycondition": "Enable" + } + }, + { + "href": "/power/0", + "rep": { + "value": false + } + }, + { + "href": "/sensors/vs/0", + "rep": {} + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.modelNum": "ARA-WW-TP1-22-COMMON|10229641|60010535001511014600083200800000", + "x.com.samsung.da.description": "ARA-WW-TP1-22-COMMON", + "x.com.samsung.da.serialNum": "**REDACTED**", + "x.com.samsung.da.otnDUID": "**REDACTED**", + "x.com.samsung.da.diagProtocolType": "WIFI_HTTPS", + "x.com.samsung.da.diagLogType": [ + "errCode", + "dump" + ], + "x.com.samsung.da.diagDumpType": "file", + "x.com.samsung.da.diagEndPoint": "SSM", + "x.com.samsung.da.diagMnid": "0AJT", + "x.com.samsung.da.diagSetupid": "010", + "x.com.samsung.da.diagMinVersion": "1.0", + "x.com.samsung.da.diagTsId": "DA01", + "x.com.samsung.da.serialNumOption": "**REDACTED**", + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Version", + "x.com.samsung.da.type": "Software", + "x.com.samsung.da.number": "02378A260327", + "x.com.samsung.da.newVersionAvailable": "0" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Version", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.number": "102296A23012700", + "x.com.samsung.da.newVersionAvailable": "0" + }, + { + "x.com.samsung.da.id": "2", + "x.com.samsung.da.description": "Version", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.number": "102295A22113000,102299A10000300", + "x.com.samsung.da.newVersionAvailable": "0" + } + ] + } + }, + { + "href": "/file/information/vs/0", + "rep": { + "x.com.samsung.timeoffset": "+08:00", + "x.com.samsung.supprtedtype": 1 + } + }, + { + "href": "/configuration/vs/0", + "rep": { + "x.com.samsung.da.region": "0000000000", + "x.com.samsung.da.airconOptionList": [ + "SingleCommand_1", + "DR", + "HOMECARE_WIZARD_V2", + "PRODUCT_GLOBAL", + "AI_RAC_GLOBAL_COOLONLY_3.0", + "AI_3.0", + "Auto_To_AI" + ] + } + }, + { + "href": "/humidity/0", + "rep": { + "humidity": 0 + } + }, + { + "href": "/humidity/vs/0", + "rep": { + "x.com.samsung.da.humidity": "0", + "x.com.samsung.da.fivepercentHumidity": "64" + } + }, + { + "href": "/drlc/0", + "rep": { + "DRLevel": 1, + "start": "2026-07-27T05:07:32Z", + "duration": 20, + "override": false + } + }, + { + "href": "/drlc/vs/0", + "rep": { + "x.com.samsung.da.drlcLevel": "1", + "x.com.samsung.da.duration": "20:47:00", + "x.com.samsung.da.drlcStartTime": "2026-07-27T05:07:32Z", + "x.com.samsung.da.override": "Off", + "x.com.samsung.da.realSaving": "Off" + } + }, + { + "href": "/availablecontrolsets/vs/0", + "rep": { + "x.com.samsung.da.sets": "000000B4012C016102490C000000", + "x.com.samsung.da.id": "RAC", + "x.com.samsung.da.version": "1.0" + } + }, + { + "href": "/keepnormalstate/vs/0", + "rep": { + "x.com.samsung.da.keepnormal": 1 + } + }, + { + "href": "/otninformation/vs/0", + "rep": { + "x.com.samsung.da.target": "", + "x.com.samsung.da.newVersionAvailable": "false", + "x.com.samsung.da.newVersionNo": "00000000", + "x.com.samsung.da.currentVersionInfo": "00000000", + "otnStatus": "None", + "flashingProgress": "", + "otnTarget": "main", + "otnCompleteDate": "noHistory", + "otnList": [ + { + "type": "WIFI", + "modelId": "ARA-WW-TP1-22-COMMON", + "versions": [ + "11260327" + ], + "visVersion": "260327" + }, + { + "type": "Micom", + "modelId": "", + "versions": [ + "23012700", + "FFFFFFFF" + ], + "visVersion": "230127" + }, + { + "type": "Micom", + "modelId": "", + "versions": [ + "23012700", + "FFFFFFFF" + ], + "visVersion": "230127" + } + ] + } + }, + { + "href": "/connectionconfig/vs/0", + "rep": { + "autoReconnectionMinVersion": "1.0", + "autoReconnection": "true", + "autoReconnectionProtocolType": [ + "helper_hotspot", + "ble_ocf" + ], + "supportedWiFiAuthType": [ + "OPEN", + "WEP", + "WPA-PSK", + "WPA2-PSK", + "SAE" + ], + "supportedWiFiCryptoType": [ + "TKIP", + "AES", + "WEP-64", + "WEP-128" + ], + "supportedWiFiFreq": [ + "2.4G" + ], + "calmConnectionCare": { + "version": "1.0", + "role": [ + "things" + ] + } + } + }, + { + "href": "/timezone/vs/0", + "rep": { + "timezoneid": "Asia/Kuala_Lumpur", + "offset": "+08:00", + "DST": "OFF" + } + }, + { + "href": "/option/muteonce/vs/0", + "rep": { + "muteonce": "Off" + } + }, + { + "href": "/aisleep/vs/0", + "rep": { + "x.com.samsung.da.displayNightMode": "Off", + "x.com.samsung.da.elapsedTime": "0", + "x.com.samsung.da.requestFeedback": "Off", + "x.com.samsung.da.resultFeedback": "0", + "x.com.samsung.da.statusFeedback": "Idle", + "x.com.samsung.da.sleepTime": "14002200" + } + }, + { + "href": "/wirelessinfo/vs/0", + "rep": { + "macaddressWiFi": "**REDACTED**", + "macaddressBLE": "**REDACTED**", + "connectedApSsid": "dreamer" + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/airconditioner_windfree_oscillation_device.json b/tests/fixtures/airconditioner_windfree_oscillation_device.json new file mode 100644 index 0000000..3bc13ee --- /dev/null +++ b/tests/fixtures/airconditioner_windfree_oscillation_device.json @@ -0,0 +1,590 @@ +{ + "device0": [ + { + "rt": [ + "x.com.samsung.devcol", + "oic.wk.col" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ] + }, + { + "href": "/aisleep/vs/0", + "rep": { + "x.com.samsung.da.displayNightMode": "Off", + "x.com.samsung.da.elapsedTime": "0", + "x.com.samsung.da.requestFeedback": "Off", + "x.com.samsung.da.resultFeedback": 0, + "x.com.samsung.da.statusFeedback": "Idle", + "x.com.samsung.da.sleepTime": "14002200" + } + }, + { + "href": "/alarms/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.alarmType": "Device", + "x.com.samsung.da.code": "ErrorCode_OFF", + "x.com.samsung.da.triggeredTime": "2026-07-27T11:59:06", + "x.com.samsung.da.state": "Deleted" + } + ], + "rt": [ + "x.com.samsung.da.alarms" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/anomalyload/vs/0", + "rep": { + "operation": "Off", + "mode": "Alarm", + "supportedModes": [ + "Alarm", + "PowerSaving" + ], + "savingTime": "20", + "supportedSavingTime": [ + "20", + "40", + "60" + ] + } + }, + { + "href": "/availablecontrolsets/vs/0", + "rep": { + "x.com.samsung.da.sets": "000000A0012C01610CC90C0F0000", + "x.com.samsung.da.id": "RAC", + "x.com.samsung.da.version": "1.0" + } + }, + { + "href": "/configuration/vs/0", + "rep": { + "x.com.samsung.da.region": "0000000000", + "x.com.samsung.da.airconOptionList": [ + "PRODUCT_GLOBAL", + "AI_GLOBAL_COOLONLY_4.0", + "AI_3.0", + "Auto_To_AI", + "ENERGY_2.0", + "HOMECARE_WIZARD_V2", + "DR", + "SingleCommand_1" + ], + "InstalledRoomSize": "0.0", + "SkipInstalledRoomSize": "false", + "RoomSizeMenuNumber": "Manual" + } + }, + { + "href": "/da/softreset/vs/0", + "rep": { + "x.com.samsung.da.softwarereset": "false" + } + }, + { + "href": "/drlc/vs/0", + "rep": { + "x.com.samsung.da.start": "1970-01-01T00:00:00Z", + "x.com.samsung.da.override": "Off", + "x.com.samsung.da.durationminutes": "0", + "x.com.samsung.da.drlcLevel": "0", + "x.com.samsung.da.realSaving": "Off" + } + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.cumulativeDate": "1785153515", + "x.com.samsung.da.cumulativeSavedPower": "0", + "x.com.samsung.da.cumulativePower": "47536", + "x.com.samsung.da.cumulativeUnit": "Wh", + "x.com.samsung.da.instantaneousPowerUnit": "W", + "x.com.samsung.da.cumulativePowerType": "total" + } + }, + { + "href": "/file/information/vs/0", + "rep": { + "x.com.samsung.timeoffset": "+08:00", + "x.com.samsung.supprtedtype": 1 + } + }, + { + "href": "/filter/airdustfilter/vs/0", + "rep": { + "x.com.samsung.da.filterUsageResolution": "1", + "x.com.samsung.da.filterCapacity": "180", + "x.com.samsung.da.filterCapacityUnit": "Hour", + "x.com.samsung.da.filterResetType": [ + "replaceable", + "washable" + ], + "x.com.samsung.da.filterStatus": "normal", + "x.com.samsung.da.filterUsage": "44", + "x.com.samsung.da.filterDesiredUsage": "180", + "x.com.samsung.da.supportedFilterDesiredUsage": [ + "180", + "300", + "500", + "700" + ] + } + }, + { + "href": "/humidity/vs/0", + "rep": { + "x.com.samsung.da.humidity": "0", + "x.com.samsung.da.fivepercentHumidity": "46" + } + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.otnDUID": "**REDACTED**", + "x.com.samsung.da.modelNum": "TP1X_DA-AC-RAC-01011_0000|10281241|6001051A001911018E0049300002A900", + "x.com.samsung.da.description": "TP1X_DA-AC-RAC-01011_0000", + "x.com.samsung.da.serialNum": "**REDACTED**", + "x.com.samsung.da.serialNumOption": "**REDACTED**", + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Version", + "x.com.samsung.da.type": "Software", + "x.com.samsung.da.number": "02825A260401", + "x.com.samsung.da.newVersionAvailable": "0" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Version", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.newVersionAvailable": "0", + "x.com.samsung.da.number": "02812A25111800,FFFFFFFFFFFFFF" + }, + { + "x.com.samsung.da.id": "2", + "x.com.samsung.da.description": "Version", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.newVersionAvailable": "0", + "x.com.samsung.da.number": "02813A25101500,FFFFFFFFFFFFFF" + }, + { + "x.com.samsung.da.id": "3", + "x.com.samsung.da.description": "Version", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.newVersionAvailable": "0", + "x.com.samsung.da.number": "FFFFFFFFFFFFFF,FFFFFFFFFFFFFF" + } + ], + "x.com.samsung.da.diagDumpType": "file", + "x.com.samsung.da.diagEndPoint": "SSM", + "x.com.samsung.da.diagLogType": [ + "errCode", + "dump" + ], + "x.com.samsung.da.diagMnid": "0AJT", + "x.com.samsung.da.diagSetupid": "AR2", + "x.com.samsung.da.diagProtocolType": "BLE_OCF", + "x.com.samsung.da.diagMinVersion": "3.0", + "x.com.samsung.da.diagTsId": "DA01" + } + }, + { + "href": "/keepnormalstate/vs/0", + "rep": { + "x.com.samsung.da.keepnormal": 0 + } + }, + { + "href": "/light/vs/0", + "rep": { + "mode": "On", + "supportedModes": [ + "On", + "Off" + ] + } + }, + { + "href": "/mds/absencemonitoring/vs/0", + "rep": {} + }, + { + "href": "/mds/absencestate/vs/0", + "rep": {} + }, + { + "href": "/mode/convenient/vs/0", + "rep": { + "x.com.samsung.da.modes": "Off", + "x.com.samsung.da.supportedModes": [ + "Off", + "Sleep", + "Quiet", + "Smart", + "Speed", + "Nano", + "NanoSleep", + "DryComfort" + ] + } + }, + { + "href": "/mode/vs/0", + "rep": { + "x.com.samsung.da.modes": [ + "Cool" + ], + "workingMode": "Cool", + "x.com.samsung.da.supportedModes": [ + "Auto", + "Cool", + "Dry", + "Fan" + ], + "x.com.samsung.da.options": [ + "OptionCode_56416", + "Sleep_0", + "SleepRemainTime_0", + "ArtificialWorking_Off", + "ComfortAICooling_Off", + "AiTempChanged_Off", + "AiTemp_240", + "Volume_100", + "DiagnosisAI_Off", + "ProgressDiagnosisAI_0", + "ResultDiagnosisAI_Normal", + "SmartCoolClean_Off", + "ProgressSmartClean_0", + "OutDoorVentil_Off", + "InDoorVentil_Off", + "FreezeAlarmSetting_Off", + "DesiredFreezeAlarm_240", + "WashAlarm_Off", + "ExtendOptionCode_246413", + "UpdateAllow_NotAllowed", + "DurationOn_0", + "WelcomeCoolingState_Off" + ], + "rt": [ + "x.com.samsung.da.mode" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/option/autoclean/vs/0", + "rep": { + "x.com.samsung.da.status": "Stop", + "x.com.samsung.da.supportedStatus": [ + "Start", + "Stop" + ], + "x.com.samsung.da.progress": "0", + "x.com.samsung.da.settingStatus": "Off", + "x.com.samsung.da.supportedSettingStatus": [ + "On", + "Off" + ], + "defaultSettingStatus": "On" + } + }, + { + "href": "/option/muteonce/vs/0", + "rep": { + "muteonce": "Off", + "rt": [ + "x.com.samsung.da.option.muteonce" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/personality/presence/vs/0", + "rep": {} + }, + { + "href": "/power/vs/0", + "rep": { + "x.com.samsung.da.power": "Off", + "operationNumber": "2", + "displayCondition": "Enable" + } + }, + { + "href": "/realtimenotiforclient/vs/0", + "rep": { + "x.com.samsung.da.timeforshortnoti": "0", + "x.com.samsung.da.longnotisubscription": "false", + "x.com.samsung.da.periodicnotisubscription": "true" + } + }, + { + "href": "/reserverulesets/vs/0", + "rep": { + "x.com.samsung.da.sets": "8EFFFFFFFF101E121EFFFFFFFFFFFF000001001F0001000000000000009C00FFFF1E00", + "x.com.samsung.da.id": "RAC", + "x.com.samsung.da.version": "1.0" + } + }, + { + "href": "/selfcheck/vs/0", + "rep": { + "x.com.samsung.da.start": "Cancel", + "x.com.samsung.da.status": "Ready", + "x.com.samsung.da.progress": "0", + "x.com.samsung.da.result": "NA", + "x.com.samsung.da.supportedActions": [ + "Start", + "Cancel" + ], + "x.com.samsung.da.error": [ + "DA_ERROR_NONE" + ] + } + }, + { + "href": "/temperature/control/vs/0", + "rep": { + "x.com.samsung.da.increment": "1" + } + }, + { + "href": "/temperatures/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Temperature", + "x.com.samsung.da.current": "29.0", + "x.com.samsung.da.desired": "18.0", + "x.com.samsung.da.minimum": "16", + "x.com.samsung.da.maximum": "30", + "x.com.samsung.da.increment": "1", + "x.com.samsung.da.unit": "Celsius" + } + ] + } + }, + { + "href": "/wind/oscillation/vs/0", + "rep": { + "vertical": "Fix", + "supportedVertical": [ + "Swing", + "Fix" + ], + "verticalAngle": "4", + "supportedVerticalAngle": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "horizontal": "Fix", + "supportedHorizontal": [ + "Swing", + "Fix" + ], + "horizontalSwingMode": "AllSwing", + "supportedHorizontalSwingMode": [ + "AllSwing", + "LeftSwing", + "RightSwing" + ], + "horizontalAngle": "3", + "supportedHorizontalAngle": [ + "1", + "2", + "3", + "4", + "5" + ] + } + }, + { + "href": "/wind/strength/vs/0", + "rep": { + "x.com.samsung.da.modes": "1", + "x.com.samsung.da.supportedModes": [ + "0", + "1", + "2", + "3", + "4" + ], + "x.com.samsung.da.modesName": [ + "Auto", + "Low", + "Mid", + "High", + "Turbo" + ] + } + }, + { + "href": "/otninformation/vs/0", + "rep": { + "x.com.samsung.da.target": "", + "x.com.samsung.da.newVersionAvailable": "false", + "otnStatus": "None", + "flashingProgress": "0", + "otnCompleteDate": "noHistory", + "scheduledTime": "None", + "swVersionInfo": { + "platform": "Tizen Lite", + "oneUiVersion": "7.0 Air conditioner", + "osVersion": "4.0" + }, + "otnList": [ + { + "type": "WIFI", + "modelId": "ARA-WW-TP1-26-AR6X00MG", + "versions": [ + "11260401" + ], + "visVersion": "260401" + }, + { + "type": "Micom", + "modelId": "845241131741FFFFFFFF", + "versions": [ + "25102900", + "FFFFFFFF" + ], + "visVersion": "251029" + }, + { + "type": "Micom", + "modelId": "045210281241FFFFFFFF", + "versions": [ + "25111800", + "FFFFFFFF" + ], + "visVersion": "251118" + }, + { + "type": "Micom", + "modelId": "045210281341FFFFFFFF", + "versions": [ + "25101500", + "FFFFFFFF" + ], + "visVersion": "251015" + }, + { + "type": "Micom", + "modelId": "0452FFFFFFFFFFFFFFFF", + "versions": [ + "FFFFFFFF", + "FFFFFFFF" + ], + "visVersion": "999999" + } + ] + } + }, + { + "href": "/timezone/vs/0", + "rep": { + "timezoneid": "Asia/Kuala_Lumpur", + "offset": "+08:00", + "DST": "OFF" + } + }, + { + "href": "/connectionconfig/vs/0", + "rep": { + "autoReconnectionMinVersion": "1.0", + "autoReconnection": "true", + "autoReconnectionProtocolType": [ + "helper_hotspot", + "ble_ocf" + ], + "supportedWiFiAuthType": [ + "OPEN", + "WEP", + "WPA-PSK", + "WPA2-PSK", + "SAE" + ], + "supportedWiFiCryptoType": [ + "TKIP", + "AES", + "WEP-64", + "WEP-128" + ], + "supportedWiFiFreq": [ + "2.4G" + ], + "calmConnectionCare": { + "version": "1.0", + "role": [ + "things" + ] + } + } + }, + { + "href": "/wirelessinfo/vs/0", + "rep": { + "macaddressWiFi": "**REDACTED**", + "macaddressBLE": "**REDACTED**", + "connectedApSsid": "K9 Unit" + } + }, + { + "href": "/quickcontrol/info/vs/0", + "rep": { + "supportedVersion": "1.0" + } + }, + { + "href": "/dginformation/vs/0", + "rep": { + "enrolmentstatus": "Unknown", + "devicestate": "Unknown", + "lockstatus": "Unknown", + "nextduedate": "", + "workingminutes": 0, + "paymentinfo": { + "emiplan": "Unknown", + "currency": "Unknown", + "totalemi": 0, + "totalemipaid": 0 + } + } + }, + { + "href": "/temperature/desired/0", + "rep": { + "temperature": 25 + } + }, + { + "href": "/power/0", + "rep": { + "value": false + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/golden/air_purifier_tp1x_da_ac_air.json b/tests/fixtures/golden/air_purifier_tp1x_da_ac_air.json new file mode 100644 index 0000000..b72b83a --- /dev/null +++ b/tests/fixtures/golden/air_purifier_tp1x_da_ac_air.json @@ -0,0 +1,27 @@ +{ + "state_keys": [ + "alarm_code", + "child_lock", + "clean_level", + "device_active", + "display", + "display_light", + "dust", + "energy_kwh", + "energy_saved_kwh", + "fan", + "fine_dust", + "firmware_update", + "hepa_filter_status", + "hepa_filter_usage", + "mute_once", + "odor", + "panel_status", + "pet_filter_activation", + "power_switch", + "sound_mode", + "sound_output", + "sound_volume", + "super_fine_dust" + ] +} \ No newline at end of file diff --git a/tests/fixtures/golden/airconditioner_ara_ww_tp1_22.json b/tests/fixtures/golden/airconditioner_ara_ww_tp1_22.json new file mode 100644 index 0000000..a2b459c --- /dev/null +++ b/tests/fixtures/golden/airconditioner_ara_ww_tp1_22.json @@ -0,0 +1,17 @@ +{ + "state_keys": [ + "air_filter_status", + "air_filter_usage", + "alarm_code", + "auto_clean", + "climate", + "current_temperature_c", + "display_light", + "energy_kwh", + "energy_saved_kwh", + "firmware_update", + "humidity", + "mute_once", + "power_watts" + ] +} \ No newline at end of file diff --git a/tests/fixtures/golden/airconditioner_windfree_oscillation.json b/tests/fixtures/golden/airconditioner_windfree_oscillation.json new file mode 100644 index 0000000..000bed7 --- /dev/null +++ b/tests/fixtures/golden/airconditioner_windfree_oscillation.json @@ -0,0 +1,21 @@ +{ + "state_keys": [ + "air_filter_status", + "air_filter_usage", + "alarm_code", + "auto_clean", + "climate", + "current_temperature_c", + "display_light", + "energy_kwh", + "energy_saved_kwh", + "firmware_update", + "humidity", + "mute_once", + "overload_protection_active", + "overload_protection_mode", + "selfcheck_error", + "selfcheck_result", + "selfcheck_status" + ] +} \ No newline at end of file diff --git a/tests/fixtures/golden/oven_mw7300b.json b/tests/fixtures/golden/oven_mw7300b.json new file mode 100644 index 0000000..435dec4 --- /dev/null +++ b/tests/fixtures/golden/oven_mw7300b.json @@ -0,0 +1,25 @@ +{ + "state_keys": [ + "alarm_code", + "child_lock", + "cloud_connected", + "cook_time", + "current_temp_c", + "cycle_active", + "door_open", + "energy_kwh", + "fast_preheat", + "finish_time", + "firmware_update", + "lamp", + "machine_state", + "natural_steam", + "operation_time_minutes", + "oven_mode", + "oven_setpoint", + "oven_state", + "progress_percentage", + "remote_control", + "sound" + ] +} \ No newline at end of file diff --git a/tests/fixtures/golden/range_ne8300d.json b/tests/fixtures/golden/range_ne8300d.json new file mode 100644 index 0000000..49f4d08 --- /dev/null +++ b/tests/fixtures/golden/range_ne8300d.json @@ -0,0 +1,27 @@ +{ + "state_keys": [ + "alarm_code", + "child_lock", + "cloud_connected", + "cook_time", + "cooktop_running_state", + "current_temp_c", + "cycle_active", + "door_open", + "fast_preheat", + "finish_time", + "firmware_update", + "lamp", + "machine_state", + "natural_steam", + "operation_time_minutes", + "oven_mode", + "oven_setpoint", + "oven_state", + "power_switch", + "progress_percentage", + "remote_control", + "sound", + "warming_center_state" + ] +} \ No newline at end of file diff --git a/tests/fixtures/golden/vacuum_station.json b/tests/fixtures/golden/vacuum_station.json new file mode 100644 index 0000000..ae896c2 --- /dev/null +++ b/tests/fixtures/golden/vacuum_station.json @@ -0,0 +1,19 @@ +{ + "state_keys": [ + "alarm_code", + "auto_empty", + "cleanstation_status", + "discharging_time", + "dustbag_full", + "dustbag_usage", + "dustbin_auto_close", + "energy_kwh", + "firmware_update", + "stick_status", + "uvc_emitted_time", + "uvc_finished_time", + "uvc_intensive_mode", + "uvc_operation_time", + "uvc_total_operation_time" + ] +} \ No newline at end of file diff --git a/tests/fixtures/golden/washer_wa55a7700av.json b/tests/fixtures/golden/washer_wa55a7700av.json new file mode 100644 index 0000000..930f097 --- /dev/null +++ b/tests/fixtures/golden/washer_wa55a7700av.json @@ -0,0 +1,30 @@ +{ + "state_keys": [ + "alarm_code", + "child_lock", + "completion_minutes", + "cycle", + "cycle_active", + "delay_start_hours", + "detergent_low", + "detergent_quantity", + "detergent_water_hardness", + "diagnosis_status", + "drum_clean_cycles_remaining", + "energy_kwh", + "finish_time", + "firmware_update", + "job_beginning_status", + "machine_state", + "power_switch", + "progress", + "progress_percentage", + "remote_control", + "rinse_cycles", + "softener_concentration", + "softener_low", + "softener_quantity", + "spin_speed", + "wash_temperature" + ] +} \ No newline at end of file diff --git a/tests/fixtures/oven_mw7300b_device.json b/tests/fixtures/oven_mw7300b_device.json new file mode 100644 index 0000000..f5a4ded --- /dev/null +++ b/tests/fixtures/oven_mw7300b_device.json @@ -0,0 +1,278 @@ +{ + "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-22-OVEN-2", + "versions": [ + "40241114" + ], + "visVersion": "241114" + }, + { + "type": "Micom", + "modelId": "074340475341FFFFFFFF", + "versions": [ + "23121600", + "FFFFFFFF" + ], + "visVersion": "231216" + }, + { + "type": "Micom", + "modelId": "074340460841FFFFFFFF", + "versions": [ + "22051300", + "FFFFFFFF" + ], + "visVersion": "220513" + } + ] + } + }, + { + "href": "/connected/vs/0", + "rep": { + "x.com.samsung.da.connected": "On" + } + }, + { + "href": "/doors/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Door", + "x.com.samsung.da.openState": "Close" + } + ], + "rt": [ + "x.com.samsung.da.doors" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.modelNum": "TP1X_DA-KS-MICROWAVE-01041|40475341|50040100021811000A00000000000000", + "x.com.samsung.da.description": "MW7300B-/EU1", + "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": "04753A23121600", + "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": "04608A22051300", + "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": "KM6", + "x.com.samsung.da.diagMinVersion": "3.0", + "x.com.samsung.da.diagTsId": "DA01" + } + }, + { + "href": "/mode/vs/0", + "rep": { + "x.com.samsung.da.supportedModes": [ + "NoOperation", + "Autocook", + "AutocookCustom", + "Convection", + "AirFryer", + "Grill", + "MicroWave", + "MicroWaveGrill", + "MicroWaveConvection", + "Deodorization" + ], + "x.com.samsung.da.modes": [ + "NoOperation" + ], + "x.com.samsung.da.options": [ + "DeviceType_MW7300B-/EU1", + "TimeAutoSync_On", + "TimeSystem_24", + "Sound_Off" + ], + "x.com.samsung.da.defaultMode": "Convection", + "x.com.samsung.da.modeSpec": "[{\"mode\":\"NoOperation\",\"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\":\"Autocook\",\"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\":\"AutocookCustom\",\"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\":\"Convection\",\"version\":\"0101\",\"default\":\"Default\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"40\",\"tempMaxC\":\"200\",\"tempDefaultC\":\"180\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:00:10\",\"timeMax\":\"01:00:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"5\",\"tempIntervalF\":\"NotSupported\",\"probeIntervalC\":\"NotSupported\",\"probeIntervalF\":\"NotSupported\"},{\"mode\":\"AirFryer\",\"version\":\"0101\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"100\",\"tempMaxC\":\"200\",\"tempDefaultC\":\"180\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:00:10\",\"timeMax\":\"01:00:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"5\",\"tempIntervalF\":\"NotSupported\",\"probeIntervalC\":\"NotSupported\",\"probeIntervalF\":\"NotSupported\"},{\"mode\":\"Grill\",\"version\":\"0101\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:00:10\",\"timeMax\":\"01:00:00\",\"timeDefault\":\"00:01:00\",\"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\":\"MicroWave\",\"version\":\"0101\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:00:10\",\"timeMax\":\"01:39:00\",\"timeDefault\":\"00:00:30\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"900W\",\"powerListLength\":\"7\",\"powerListData\":[\"100W\",\"180W\",\"300W\",\"450W\",\"600W\",\"700W\",\"900W\"],\"tempIntervalC\":\"NotSupported\",\"tempIntervalF\":\"NotSupported\",\"probeIntervalC\":\"NotSupported\",\"probeIntervalF\":\"NotSupported\"},{\"mode\":\"MicroWaveGrill\",\"version\":\"0101\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:00:10\",\"timeMax\":\"01:00:00\",\"timeDefault\":\"00:00:30\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"600W\",\"powerListLength\":\"5\",\"powerListData\":[\"100W\",\"180W\",\"300W\",\"450W\",\"600W\"],\"tempIntervalC\":\"NotSupported\",\"tempIntervalF\":\"NotSupported\",\"probeIntervalC\":\"NotSupported\",\"probeIntervalF\":\"NotSupported\"},{\"mode\":\"MicroWaveConvection\",\"version\":\"0101\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"40\",\"tempMaxC\":\"200\",\"tempDefaultC\":\"180\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:00:10\",\"timeMax\":\"01:00:00\",\"timeDefault\":\"00:00:30\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"600W\",\"powerListLength\":\"5\",\"powerListData\":[\"100W\",\"180W\",\"300W\",\"450W\",\"600W\"],\"tempIntervalC\":\"5\",\"tempIntervalF\":\"NotSupported\",\"probeIntervalC\":\"NotSupported\",\"probeIntervalF\":\"NotSupported\"},{\"mode\":\"Deodorization\",\"version\":\"0101\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:00:10\",\"timeMax\":\"00:15:00\",\"timeDefault\":\"00:05:00\",\"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": { + "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": "0", + "rt": [ + "x.com.samsung.da.operation" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "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-27T08:26:55" + } + ] + } + }, + { + "href": "/oven/vs/0", + "rep": { + "x.com.samsung.da.state": "Ready", + "x.com.samsung.da.recipe": "00000000000000", + "x.com.samsung.da.powerLevel": "0W", + "rt": [ + "x.com.samsung.da.oven" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "href": "/temperatures/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Temperature", + "x.com.samsung.da.desired": "0", + "x.com.samsung.da.current": "1", + "x.com.samsung.da.increment": "5", + "x.com.samsung.da.unit": "Celsius" + } + ], + "rt": [ + "x.com.samsung.da.temperatures" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/timezone/vs/0", + "rep": { + "timezoneid": "Europe/London", + "offset": "+01:00", + "DST": "ON" + } + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.instantaneousPower": "-500", + "x.com.samsung.da.instantaneousPowerUnit": "W", + "x.com.samsung.da.cumulativePower": "91800", + "x.com.samsung.da.cumulativeUnit": "Wh" + } + }, + { + "href": "/kidslock/vs/0", + "rep": { + "x.com.samsung.da.kidsLock": "Ready" + } + }, + { + "href": "/recipe/cook/vs/0", + "rep": { + "x.com.samsung.da.recipeDisplay": "{\"language\":\"None\",\"menu\":\"\",\"servingSize\":\"\",\"option\":\"\"}" + } + }, + { + "href": "/remotectrl/vs/0", + "rep": { + "x.com.samsung.da.remoteControlEnabled": "false", + "rt": [ + "x.com.samsung.da.configuration" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "href": "/wirelessinfo/vs/0", + "rep": { + "macaddressWiFi": "**REDACTED**", + "macaddressBLE": "**REDACTED**" + } + }, + { + "href": "/quickcontrol/info/vs/0", + "rep": { + "supportedVersion": "1.0" + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/range_ne8300d_device.json b/tests/fixtures/range_ne8300d_device.json new file mode 100644 index 0000000..75e7e61 --- /dev/null +++ b/tests/fixtures/range_ne8300d_device.json @@ -0,0 +1,322 @@ +{ + "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": "071240473041FFFFFFFF", + "versions": [ + "23123000", + "FFFFFFFF" + ], + "visVersion": "231230" + }, + { + "type": "Micom", + "modelId": "071260141341FFFFFFFF", + "versions": [ + "24010300", + "FFFFFFFF" + ], + "visVersion": "240103" + } + ] + } + }, + { + "href": "/connected/vs/0", + "rep": { + "x.com.samsung.da.connected": "On", + "rt": [ + "x.com.samsung.da.connected" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "href": "/doors/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Door", + "x.com.samsung.da.openState": "Close", + "x.com.samsung.da.lock": "Unlock" + } + ], + "rt": [ + "x.com.samsung.da.doors" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.modelNum": "TP1X_DA-KS-RANGE-0102X|40473041|5001011E031811010A00000000000000", + "x.com.samsung.da.description": "NE8300D-/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": "04730A23123000", + "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": "01413A24010300", + "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": "KR2", + "x.com.samsung.da.diagMinVersion": "3.0", + "x.com.samsung.da.diagTsId": "DA01" + } + }, + { + "href": "/mode/vs/0", + "rep": { + "x.com.samsung.da.supportedModes": [ + "ConvectionBake", + "ConvectionRoast", + "Bake", + "Broil", + "AirFryer", + "KeepWarm", + "BreadProof", + "Dehydrate", + "SelfClean", + "SteamClean" + ], + "x.com.samsung.da.modes": [ + "NoOperation" + ], + "x.com.samsung.da.options": [ + "DeviceType_NE8300D-/AA0", + "SettingPossible_0", + "TimeAutoSync_On", + "UpperLamp_Off", + "TimeSystem_12", + "Sound_On", + "AdjustingTemp_147", + "Sabbath_Off", + "EnergySaving_On", + "BurnerOnAlter_On" + ], + "x.com.samsung.da.defaultMode": "Bake", + "x.com.samsung.da.modeSpec": "[{\"mode\":\"ConvectionBake\",\"version\":\"0101\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"160\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"325\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"40\",\"probeMaxC\":\"90\",\"probeDefaultC\":\"65\",\"probeMinF\":\"100\",\"probeMaxF\":\"200\",\"probeDefaultF\":\"150\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"1\",\"tempIntervalF\":\"1\",\"probeIntervalC\":\"1\",\"probeIntervalF\":\"1\"},{\"mode\":\"ConvectionRoast\",\"version\":\"0101\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"160\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"325\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"40\",\"probeMaxC\":\"90\",\"probeDefaultC\":\"65\",\"probeMinF\":\"100\",\"probeMaxF\":\"200\",\"probeDefaultF\":\"150\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"1\",\"tempIntervalF\":\"1\",\"probeIntervalC\":\"1\",\"probeIntervalF\":\"1\"},{\"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\":\"1\",\"tempIntervalF\":\"1\",\"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\":\"AirFryer\",\"version\":\"0101\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"175\",\"tempMaxC\":\"260\",\"tempDefaultC\":\"220\",\"tempListLengthC\":\"0\",\"tempMinF\":\"350\",\"tempMaxF\":\"500\",\"tempDefaultF\":\"425\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"1\",\"tempIntervalF\":\"1\",\"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\":\"Dehydrate\",\"version\":\"0101\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"40\",\"tempMaxC\":\"105\",\"tempDefaultC\":\"65\",\"tempListLengthC\":\"0\",\"tempMinF\":\"100\",\"tempMaxF\":\"225\",\"tempDefaultF\":\"150\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"1\",\"tempIntervalF\":\"1\",\"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\"}]", + "rt": [ + "x.com.samsung.da.mode" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/operational/state/vs/0", + "rep": { + "x.com.samsung.da.state": "Ready", + "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", + "rt": [ + "x.com.samsung.da.operation" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "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-27T04:46:16" + } + ] + } + }, + { + "href": "/oven/vs/0", + "rep": { + "x.com.samsung.da.state": "Ready", + "rt": [ + "x.com.samsung.da.oven" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "href": "/temperatures/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Temperature", + "x.com.samsung.da.desired": "0", + "x.com.samsung.da.current": "175", + "x.com.samsung.da.increment": "1", + "x.com.samsung.da.unit": "Fahrenheit" + } + ], + "rt": [ + "x.com.samsung.da.temperatures" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/timezone/vs/0", + "rep": { + "timezoneid": "America/Phoenix", + "offset": "-07:00", + "DST": "OFF" + } + }, + { + "href": "/oven/spec/vs/0", + "rep": { + "cavityInfo": { + "numberOfCavity": 1, + "type": "single", + "serviceMainCavity": "single", + "cavityList": [ + { + "name": "single", + "flexSupport": false, + "single": { + "resourceId": 0, + "supportedFeatureList": [ + "" + ] + } + } + ] + } + } + }, + { + "href": "/cooktopmonitoring/vs/0", + "rep": { + "x.com.samsung.da.supportedCooktopRunningState": [ + "Run", + "Ready" + ], + "x.com.samsung.da.cooktopMonitoring": "0", + "x.com.samsung.da.warmingCenterState": "Off", + "x.com.samsung.da.cooktopRunningState": "Ready" + } + }, + { + "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", + "rt": [ + "x.com.samsung.da.configuration" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "href": "/wirelessinfo/vs/0", + "rep": { + "macaddressWiFi": "**REDACTED**", + "macaddressBLE": "**REDACTED**" + } + }, + { + "href": "/quickcontrol/info/vs/0", + "rep": { + "supportedVersion": "1.0" + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/vacuum_station_device.json b/tests/fixtures/vacuum_station_device.json new file mode 100644 index 0000000..962a252 --- /dev/null +++ b/tests/fixtures/vacuum_station_device.json @@ -0,0 +1,170 @@ +{ + "device0": [ + { + "rt": [ + "x.com.samsung.devcol", + "oic.wk.col" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ] + }, + { + "href": "/realtimenotiforclient/vs/0", + "rep": { + "x.com.samsung.da.timeforshortnoti": "0", + "x.com.samsung.da.timeforlongnoti": "false", + "x.com.samsung.da.periodicnotisubscription": "true" + } + }, + { + "href": "/alarms/vs/0", + "rep": {} + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.cumulativePower": "915", + "x.com.samsung.da.cumulativeUnit": "Wh" + } + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.modelNum": "A-VSKR-TP1-22-VS9500AL|50023541|80030100001611000800000000000000", + "x.com.samsung.da.description": "A-VSKR-TP1-22-VS9500AL", + "x.com.samsung.da.serialNum": "**REDACTED**", + "x.com.samsung.da.otnDUID": "**REDACTED**", + "x.com.samsung.da.diagProtocolType": "WIFI_HTTPS", + "x.com.samsung.da.diagLogType": [ + "errCode", + "dump" + ], + "x.com.samsung.da.diagDumpType": "file", + "x.com.samsung.da.diagEndPoint": "SSM", + "x.com.samsung.da.diagMnid": "0AJT", + "x.com.samsung.da.diagSetupid": "VS1", + "x.com.samsung.da.diagMinVersion": "1.0", + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Version", + "x.com.samsung.da.type": "Software", + "x.com.samsung.da.number": "023741250507", + "x.com.samsung.da.newVersionAvailable": "0" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Version", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.number": "25050800", + "x.com.samsung.da.newVersionAvailable": "0" + } + ] + } + }, + { + "href": "/file/information/vs/0", + "rep": { + "x.com.samsung.timeoffset": "+09:00", + "x.com.samsung.supprtedtype": 1 + } + }, + { + "href": "/configuration/vs/0", + "rep": { + "x.com.samsung.da.region": "2650000000", + "x.com.samsung.da.airconOptionList": [ + "HOMECARE_WIZARD_V2", + "CLEAN_OPTION_SUPPORT" + ] + } + }, + { + "href": "/component/station/dustbag/vs/0", + "rep": { + "x.com.samsung.da.status": "normal", + "x.com.samsung.da.supportedStatus": [ + "full", + "normal" + ] + } + }, + { + "href": "/setting/dustbin/vs/0", + "rep": { + "x.com.samsung.da.autoEmpty": "On", + "x.com.samsung.da.autoClose": "On", + "x.com.samsung.da.desiredDischargingTime": "3", + "x.com.samsung.da.supportedDischargingTime": [ + "1", + "3" + ] + } + }, + { + "href": "/status/cleanstation/vs/0", + "rep": { + "x.com.samsung.da.status": "Ready", + "x.com.samsung.da.uvcTotalOperationTime": "90", + "x.com.samsung.da.uvcOperationTime": "90", + "x.com.samsung.da.uvcFinishedTime": "2026-07-27T10:26Z", + "x.com.samsung.da.emittedTime": "2026-07-26T12:26Z", + "x.com.samsung.da.uvcIntensive": "Off", + "x.com.samsung.da.stickStatus": "On" + } + }, + { + "href": "/component/station/dustbagusage/vs/0", + "rep": { + "x.com.samsung.da.dustbagUsage": "506", + "x.com.samsung.da.dustbagPrevUsage": "0" + } + }, + { + "href": "/otninformation/vs/0", + "rep": { + "x.com.samsung.da.target": "", + "x.com.samsung.da.newVersionAvailable": "false", + "flashingProgress": "" + } + }, + { + "href": "/timezone/vs/0", + "rep": { + "timezoneid": "Asia/Seoul", + "offset": "+09:00", + "DST": "OFF" + } + }, + { + "href": "/connectionconfig/vs/0", + "rep": { + "autoReconnectionMinVersion": "1.0", + "autoReconnection": "true", + "autoReconnectionProtocolType": [ + "helper_hotspot", + "ble_ocf" + ], + "supportedWiFiAuthType": [ + "OPEN", + "WEP", + "WPA-PSK", + "WPA2-PSK", + "SAE" + ], + "supportedWiFiCryptoType": [ + "TKIP", + "AES", + "WEP-64", + "WEP-128" + ], + "supportedWiFiFreq": [ + "2.4G" + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/washer_wa55a7700av_device.json b/tests/fixtures/washer_wa55a7700av_device.json new file mode 100644 index 0000000..d52657b --- /dev/null +++ b/tests/fixtures/washer_wa55a7700av_device.json @@ -0,0 +1,407 @@ +{ + "device0": [ + { + "rt": [ + "x.com.samsung.devcol", + "oic.wk.col" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ] + }, + { + "href": "/realtimenotiforclient/vs/0", + "rep": { + "x.com.samsung.da.timeforshortnoti": "0", + "x.com.samsung.da.periodicnotisubscription": "true" + } + }, + { + "href": "/alarms/vs/0", + "rep": {} + }, + { + "href": "/diagnosis/vs/0", + "rep": { + "x.com.samsung.da.diagnosisStart": "Ready" + } + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.instantaneousPower": "-500", + "x.com.samsung.da.instantaneousPowerUnit": "W", + "x.com.samsung.da.cumulativePower": "40500", + "x.com.samsung.da.cumulativeUnit": "Wh", + "x.com.samsung.da.cumulativeDate": "1784988000", + "x.com.samsung.da.cumulativeDateUTC": "1785013200" + } + }, + { + "href": "/energy/consumption/0", + "rep": {} + }, + { + "href": "/course/vs/0", + "rep": { + "x.com.samsung.da.supportedModes": [ + "HOMECARE_WIZARD_V2" + ], + "x.com.samsung.da.options": [ + "DeviceType_0145", + "UpdateAllow_NotAllowed", + "Course_01", + "LaundryOutTime_0", + "AddGarmentIndicator_Off", + "SeamlessControl_Disable", + "KidsLockBypass_On", + "WashingTimes_18", + "DrumCleanProposal_20", + "DetergentOnce_0", + "DetergentLeft_0", + "DetergentBase_0", + "DetergentAlarm_Off", + "DetergentType_0", + "DetergentTotal_0", + "SoftenerOnce_0", + "SoftenerLeft_0", + "SoftenerBase_0", + "SoftenerAlarm_Off", + "SoftenerType_0", + "SoftenerTotal_0", + "SpecialFunction_6", + "AvailableDelayTime_67", + "DetergentLevelCtrl_0", + "SoftenerLevelCtrl_3", + "SupportedDetergentLevelCtrl_00010203", + "SupportedSoftenerLevelCtrl_00010203", + "DetergentLevel2Ctrl_1", + "SoftenerLevel2Ctrl_1", + "SupportedDetergentLevel2Ctrl_010203", + "SupportedSoftenerLevel2Ctrl_010203", + "LaundryPlannerUserSetTime_0", + "ProgressTimeSet_4205DC8208E8A206AE", + "SuperSpeed_Off", + "SendToDevice_Off", + "DetergentCycleUsed_0", + "SoftenerCycleUsed_0", + "CloudCourse_001C590549164D114A224C2037F0AC22", + "CloudExtraCourse_5958", + "OneTimeCloudCourse_FFFF010049004D004A804C0037F0AC00", + "EnergyLevelSet_05020403040204040401030203", + "MostUsed_01843E913FA43BC33E", + "SuperSpeedSet_F0F000F00000000000000000", + "DispenserSet_22222222222222FFF2000022", + "GeoFenceAlarm", + "UsagesDB_ok", + "EnergyKW_396", + "DrumCleanLog_2025-09-07T18:15:52|2025-10-05T18:49:21|2025-11-23T18:20:07|2026-01-01T19:11:59|2026-02-15T03:36:21|2026-03-09T19:59:19|2026-04-19T21:35:33|2026-05-10T19:21:00|2026-06-12T21:32:16|2026-06-29T06:29:50", + "TimeSync_NotSupported" + ], + "x.com.samsung.da.supportedOptions": [ + "401843E913FA43BC33E53853E913FA43FC53E86843E913FA43FC33E7C853E923FA43FC53E65821E913FA43FC33E7D843E923FA31FC33E1783089102A410C3087E85209204A308C0005E8000913FA43FC00055831E913FA31FC20E57821E913FA20FC10204843E913FA43FC13E" + ] + } + }, + { + "href": "/power/vs/0", + "rep": { + "x.com.samsung.da.power": "Off" + } + }, + { + "href": "/power/0", + "rep": { + "value": false + } + }, + { + "href": "/cycleinterface/vs/0", + "rep": {} + }, + { + "href": "/kidslock/vs/0", + "rep": { + "x.com.samsung.da.kidsLock": "Ready" + } + }, + { + "href": "/kidslock/0", + "rep": { + "value": false + } + }, + { + "href": "/operational/state/vs/0", + "rep": { + "x.com.samsung.da.state": "Ready", + "x.com.samsung.da.remainingTime": "01:07:00", + "x.com.samsung.da.progressPercentage": "1", + "x.com.samsung.da.progress": "None", + "x.com.samsung.da.delayEndTime": "00:00:00", + "x.com.samsung.da.supportedProgress": [ + "None", + "Weightsensing", + "Wash", + "Rinse", + "Spin", + "Finish" + ] + } + }, + { + "href": "/operational/state/0", + "rep": { + "currentMachineState": "**REDACTED**", + "machineStates": "**REDACTED**", + "jobStates": [ + "None", + "Weightsensing", + "Wash", + "Rinse", + "Spin", + "Finish" + ], + "currentJobState": "None", + "remainingTime": "01:07:00", + "progressPercentage": "1" + } + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.modelNum": "DA_WM_TP1_21_COMMON|20233742|20000001001511200AAB020200000000", + "x.com.samsung.da.description": "DA_WM_TP1_21_COMMON_WA7700A/DC92-03005B_004A", + "x.com.samsung.da.serialNum": "**REDACTED**", + "x.com.samsung.da.otnDUID": "**REDACTED**", + "x.com.samsung.da.diagProtocolType": "BLE_OCF", + "x.com.samsung.da.diagLogType": [ + "errCode", + "dump" + ], + "x.com.samsung.da.diagDumpType": "file", + "x.com.samsung.da.diagEndPoint": "SSM", + "x.com.samsung.da.diagMnid": "0AJT", + "x.com.samsung.da.diagSetupid": "210", + "x.com.samsung.da.diagMinVersion": "3.0", + "x.com.samsung.da.diagTsId": "DA01", + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "DA_WM_TP1_21_COMMON|20233742|20000001001511200AAB020200000000", + "x.com.samsung.da.type": "Software", + "x.com.samsung.da.number": "02986A260118(A182)", + "x.com.samsung.da.newVersionAvailable": "0" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Firmware_1_DB_20233742210931500FFFFF203005422212139101FFFF(01452023374220300542_30221213)(FileDown:0)(Type:0)", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.number": "02337B21093150,03005B22121391", + "x.com.samsung.da.newVersionAvailable": "0" + }, + { + "x.com.samsung.da.id": "2", + "x.com.samsung.da.description": "Firmware_2_DB_2023394522081051012FFFFFFFFFFFFFFFFFFFFFFFFE(014520233945FFFFFFFF_30000000)(FileDown:0)(Type:0)", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.number": "02339E22081051,FFFFFFFFFFFFFF" + } + ] + } + }, + { + "href": "/file/information/vs/0", + "rep": { + "x.com.samsung.timeoffset": "-07:00" + } + }, + { + "href": "/washer/vs/0", + "rep": { + "x.com.samsung.da.detergentLevel": "1", + "x.com.samsung.da.softenerLevel": "2", + "x.com.samsung.da.waterTemperature": "Warm", + "x.com.samsung.da.supportedWaterTemperature": [ + "None", + "Cold", + "Cool", + "EcoWarm", + "Warm", + "Hot" + ], + "x.com.samsung.da.spinLevel": "High", + "x.com.samsung.da.supportedSpinLevel": [ + "RinseHold", + "NoSpin", + "Low", + "Medium", + "High", + "ExtraHigh" + ], + "x.com.samsung.da.soilLevel": "Normal", + "x.com.samsung.da.supportedSoilLevel": [ + "None", + "ExtraLight", + "Light", + "Normal", + "Heavy", + "ExtraHeavy" + ], + "x.com.samsung.da.rinseCycles": "1", + "x.com.samsung.da.supportedRinseCycles": [ + "0", + "1", + "2", + "3", + "4", + "5" + ] + } + }, + { + "href": "/st/washercourse/vs/0", + "rep": { + "x.com.samsung.da.st.washerMode": "Table_02_Course_01", + "x.com.samsung.da.st.courseTable": "Table_02" + } + }, + { + "href": "/setting/vs/0", + "rep": {} + }, + { + "href": "/wm/editcourse/vs/0", + "rep": {} + }, + { + "href": "/wm/setinfo/vs/0", + "rep": { + "x.com.samsung.da.isModelSettingWithoutSC": "true", + "x.com.samsung.da.isModelSettingPowerOnOff": "false", + "x.com.samsung.da.modelCode": "M(None),W(WA55A7700AV/US)" + } + }, + { + "href": "/wm/jobbeginingstatus/vs/0", + "rep": { + "x.com.samsung.da.currentStatus": "None" + } + }, + { + "href": "/otninformation/vs/0", + "rep": { + "x.com.samsung.da.target": "", + "x.com.samsung.da.newVersionAvailable": "false", + "x.com.samsung.da.newVersionNo": "00000000", + "x.com.samsung.da.currentVersionInfo": "00000000", + "otnStatus": "None", + "flashingProgress": "", + "otnTarget": "main", + "otnCompleteDate": "2026-03-04", + "otnList": [ + { + "type": "WIFI", + "modelId": "DA_WM_TP1_21_COMMON", + "versions": [ + "30260118" + ], + "visVersion": "260118" + }, + { + "type": "Micom", + "modelId": "01452023374220300542", + "versions": [ + "21093150", + "22121391" + ], + "visVersion": "221213" + }, + { + "type": "Micom", + "modelId": "014520233945FFFFFFFF", + "versions": [ + "22081051", + "FFFFFFFF" + ], + "visVersion": "220810" + } + ] + } + }, + { + "href": "/remotectrl/vs/0", + "rep": { + "x.com.samsung.da.remoteControlEnabled": "false" + } + }, + { + "href": "/remotectrl/0", + "rep": { + "value": false + } + }, + { + "href": "/configuration/vs/0", + "rep": { + "x.com.samsung.da.region": "0000000000", + "x.com.samsung.da.countryCode": "US" + } + }, + { + "href": "/timezone/vs/0", + "rep": { + "timezoneid": "America/Phoenix", + "offset": "-07:00", + "DST": "OFF" + } + }, + { + "href": "/connectionconfig/vs/0", + "rep": { + "autoReconnectionMinVersion": "1.0", + "autoReconnection": "true", + "autoReconnectionProtocolType": [ + "helper_hotspot", + "ble_ocf" + ], + "supportedWiFiAuthType": [ + "OPEN", + "WEP", + "WPA-PSK", + "WPA2-PSK", + "SAE" + ], + "supportedWiFiCryptoType": [ + "TKIP", + "AES", + "WEP-64", + "WEP-128" + ], + "supportedWiFiFreq": [ + "2.4G" + ], + "calmConnectionCare": { + "version": "1.0", + "role": [ + "things" + ] + } + } + }, + { + "href": "/wirelessinfo/vs/0", + "rep": { + "macaddressWiFi": "**REDACTED**", + "macaddressBLE": "**REDACTED**" + } + }, + { + "href": "/quickcontrol/info/vs/0", + "rep": { + "supportedVersion": "1.0" + } + } + ] +} \ No newline at end of file diff --git a/tests/test_air_purifier_fan_discriminator.py b/tests/test_air_purifier_fan_discriminator.py new file mode 100644 index 0000000..0f61a12 --- /dev/null +++ b/tests/test_air_purifier_fan_discriminator.py @@ -0,0 +1,39 @@ +"""Tests for the /mode/vs/0 match_fn discriminator in capabilities/ +air_purifier.py (issue #130). + +`_has_top_level_modes` decides whether a given air-purifier dump's shared +/mode/vs/0 href binds to the older ARTIK051_TVTL family's MODE capability +(packed options[] scheme) or the newer TP1X_DA-AC-AIR family's FAN +capability (modes/supportedModes reported directly). Pure function, no +coordinator/entity dependency needed to test it directly -- same rationale +as test_climate_wind_oscillation_fallback.py's `_oscillation_swing`. +""" +from custom_components.localthings.registry.capabilities.air_purifier import ( + _has_top_level_modes, +) + + +def test_new_family_with_supported_modes_list_matches(): + rep = { + 'x.com.samsung.da.modes': ['Smart'], + 'x.com.samsung.da.supportedModes': ['Smart', 'Max', 'Mid', 'WindFree', 'Sleep'], + } + assert _has_top_level_modes(rep, {}) is True + + +def test_old_family_with_no_supported_modes_field_does_not_match(): + """The ARTIK051_TVTL family's /mode/vs/0 packs everything into + options[] (Light_On, Comode_Off, ...) and has no top-level + supportedModes at all.""" + rep = { + 'x.com.samsung.da.options': ['Light_On', 'Comode_Off', 'OptionCode_60282'], + } + assert _has_top_level_modes(rep, {}) is False + + +def test_non_list_supported_modes_does_not_match(): + assert _has_top_level_modes({'x.com.samsung.da.supportedModes': 'Smart'}, {}) is False + + +def test_empty_rep_does_not_match(): + assert _has_top_level_modes({}, {}) is False diff --git a/tests/test_by_type.py b/tests/test_by_type.py index e135584..15ac1e2 100644 --- a/tests/test_by_type.py +++ b/tests/test_by_type.py @@ -314,6 +314,51 @@ class TestForDeviceByModel: assert reg is not None assert reg.name == 'airconditioner' + def test_airconditioner_via_ara_ww_token(self): + """Issues #115/#116/#117/#120: ARA-WW-TP1-22-COMMON wall-mount RACs + (AR10/13/18BYEAAWKNME) report no oneUiVersion and no + '_RAC_'/'-RAC-'/'_PRAC_' token at all -- falls back to the + 'ARA-WW-' token in modelNum, reusing the same airconditioner + registry as every other TP1X-class room AC rather than a new + device type.""" + from custom_components.localthings.registry.by_type import for_device_by_model + reg = for_device_by_model( + 'ARA-WW-TP1-22-COMMON|10229641|6001051A001511014600083200800000', + 'ARA-WW-TP1-22-COMMON', + ) + assert reg is not None + assert reg.name == 'airconditioner' + + def test_oven_via_microwave_token(self): + """Issue #121: a combi microwave (MW7300B-/EU1) reports no + oneUiVersion and an unrecognized consumer token; falls back to the + '-MICROWAVE-' token in modelNum onto the *existing* oven registry + (same '/oven/vs/0' cavity + cook-mode shape), not a new device + type.""" + from custom_components.localthings.registry.by_type import for_device_by_model + reg = for_device_by_model( + 'TP1X_DA-KS-MICROWAVE-01041|40475341|50040100021811000A00000000000000', + 'MW7300B-/EU1', + ) + assert reg is not None + assert reg.name == 'oven' + + def test_vacuum_station_via_vskr_token(self): + """Issue #131: a stick-vacuum clean/auto-empty station + (A-VSKR-TP1-22-VS9500AL) reports no oneUiVersion and no + washer/dryer/dishwasher consumer prefix; falls back to the + '-VSKR-' token in modelNum onto a new vacuum_station registry (its + dump has no vacuum-body state at all, only station-specific + dustbag/dustbin/UV-sanitize resources -- see + capabilities/vacuum_station.py's module docstring).""" + from custom_components.localthings.registry.by_type import for_device_by_model + reg = for_device_by_model( + 'A-VSKR-TP1-22-VS9500AL|50023541|80030100001611000800000000000000', + 'A-VSKR-TP1-22-VS9500AL', + ) + assert reg is not None + assert reg.name == 'vacuum_station' + def test_cooktop_via_legacy_model_description(self): """Older cooktops identify themselves as ARTIK051_GLOBAL_COOKTOP.""" from custom_components.localthings.registry.by_type import for_device_by_model diff --git a/tests/test_climate_wind_oscillation_fallback.py b/tests/test_climate_wind_oscillation_fallback.py new file mode 100644 index 0000000..d9f5292 --- /dev/null +++ b/tests/test_climate_wind_oscillation_fallback.py @@ -0,0 +1,33 @@ +"""Tests for the /wind/oscillation/vs/0 swing fallback helper in climate.py +(issue #126). + +`_oscillation_swing` is a pure module-level function, like `_temps_vs_item` +in test_climate_temperature_fallback.py -- testable directly without a +coordinator/entity. This covers the fallback-selection logic that the +registry-level "no unbound hrefs" test in test_golden_regression.py doesn't +reach: newer WindFree firmware reports no /wind/direction/vs/0 at all, so +swing_mode/swing_modes would silently be None/empty without this fallback. +""" +from custom_components.localthings.climate import _oscillation_swing + + +def test_oscillation_swing_off_when_both_fixed(): + assert _oscillation_swing({'vertical': 'Fix', 'horizontal': 'Fix'}) == 'off' + + +def test_oscillation_swing_vertical_only(): + assert _oscillation_swing({'vertical': 'Swing', 'horizontal': 'Fix'}) == 'vertical' + + +def test_oscillation_swing_horizontal_only(): + assert _oscillation_swing({'vertical': 'Fix', 'horizontal': 'Swing'}) == 'horizontal' + + +def test_oscillation_swing_both(): + assert _oscillation_swing({'vertical': 'Swing', 'horizontal': 'Swing'}) == 'both' + + +def test_oscillation_swing_none_when_resource_absent(): + """Boards with /wind/direction/vs/0 instead don't populate this + resource at all -- callers must fall through cleanly, not raise.""" + assert _oscillation_swing({}) is None diff --git a/tests/test_golden_regression.py b/tests/test_golden_regression.py index 30cf536..96f7c32 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -493,6 +493,142 @@ def test_registry_reproduces_golden_state_keys_for_oven(): ) +def test_registry_reproduces_golden_state_keys_for_washer_wa55a7700av(): + """DA_WM_TP1_21_COMMON top-load washer (model WA55A7700AV, issue #111) + -- a different board generation than the WA8000T's TP2_20_COMMON, + reached through the same 'WA' consumer-model-prefix fallback (issue + #106). Binds cleanly against the existing washer registry with zero + unbound hrefs.""" + from tests.conftest import _load_device + resources = _load_device('washer_wa55a7700av') + golden = json.loads((GOLDEN / 'washer_wa55a7700av.json').read_text()) + state_keys = _new_state_keys('washer_wa55a7700av', resources) + assert set(state_keys) == set(golden['state_keys']), ( + f"state_keys mismatch:\n" + f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n" + f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}" + ) + + +def test_registry_reproduces_golden_state_keys_for_range_ne8300d(): + """TP1X_DA-KS-RANGE-0102X (model NE8300D, issue #112) -- reports no + oneUiVersion; resolved via the '-RANGE-' modelNum token fallback. + Binds cleanly against the existing range registry, including + /cooktopmonitoring/vs/0 via range.COOKTOP_MONITORING, with zero + unbound hrefs.""" + from tests.conftest import _load_device + resources = _load_device('range_ne8300d') + golden = json.loads((GOLDEN / 'range_ne8300d.json').read_text()) + state_keys = _new_state_keys('range_ne8300d', resources) + assert set(state_keys) == set(golden['state_keys']), ( + f"state_keys mismatch:\n" + f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n" + f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}" + ) + + +def test_registry_reproduces_golden_state_keys_for_airconditioner_ara_ww_tp1_22(): + """ARA-WW-TP1-22-COMMON wall-mount RACs (model AR10/13/18BYEAAWKNME, + issues #115/#116/#117/#120) report no oneUiVersion and no + '_RAC_'/'-RAC-' token -- resolved via the 'ARA-WW-' modelNum fallback. + Same resource surface as the other TP1X-class room ACs; binds cleanly + against the existing airconditioner registry with zero unbound hrefs, + so no new device type was needed.""" + from tests.conftest import _load_device + resources = _load_device('airconditioner_ara_ww_tp1_22') + golden = json.loads((GOLDEN / 'airconditioner_ara_ww_tp1_22.json').read_text()) + state_keys = _new_state_keys('airconditioner_ara_ww_tp1_22', resources) + assert set(state_keys) == set(golden['state_keys']), ( + f"state_keys mismatch:\n" + f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n" + f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}" + ) + + +def test_registry_reproduces_golden_state_keys_for_airconditioner_windfree_oscillation(): + """TP1X_DA-AC-RAC-01011_0000 Bespoke AI WindFree Deluxe (model + AR60H10D1JWNME, issue #126) -- the same board family as the + airconditioner_tp1x_da_ac_rac_01011 fixture, but a newer firmware + variant reporting no /wind/direction/vs/0 at all: swing lives on the + 2-axis /wind/oscillation/vs/0 resource instead (airconditioner. + HREF_WIND_OSCILLATION, climate.py's oscillation fallback), and it adds + an /anomalyload/vs/0 overload-response resource (airconditioner. + ANOMALY_LOAD, read-only per the 'don't guess' rule). Binds cleanly + with zero unbound hrefs.""" + from tests.conftest import _load_device + resources = _load_device('airconditioner_windfree_oscillation') + golden = json.loads( + (GOLDEN / 'airconditioner_windfree_oscillation.json').read_text() + ) + state_keys = _new_state_keys('airconditioner_windfree_oscillation', resources) + assert set(state_keys) == set(golden['state_keys']), ( + f"state_keys mismatch:\n" + f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n" + f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}" + ) + + +def test_registry_reproduces_golden_state_keys_for_oven_mw7300b(): + """TP1X_DA-KS-MICROWAVE-01041 combi microwave (model MW7300B, issue + #121) -- reports no oneUiVersion; resolved via the '-MICROWAVE-' + modelNum token fallback onto the *existing* oven registry rather than + a new device type, since it shares the same '/oven/vs/0' cavity and + '/mode/vs/0' cook-mode shape (Convection/AirFryer/Grill/MicroWave*). + The only href the oven registry didn't already cover was + /recipe/cook/vs/0 (oven.OVEN_RECIPE_COOK, an empty quick-recipe-display + blob with no entity).""" + from tests.conftest import _load_device + resources = _load_device('oven_mw7300b') + golden = json.loads((GOLDEN / 'oven_mw7300b.json').read_text()) + state_keys = _new_state_keys('oven_mw7300b', resources) + assert set(state_keys) == set(golden['state_keys']), ( + f"state_keys mismatch:\n" + f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n" + f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}" + ) + + +def test_registry_reproduces_golden_state_keys_for_air_purifier_tp1x_da_ac_air(): + """TP1X_DA-AC-AIR-01031_0000 (issue #130) self-reports oneUiVersion + '7.0 Air purifier' and resolves via for_device() onto the existing + air_purifier registry (shared with the older ARTIK051_TVTL family via + per-href match_fn discrimination -- see capabilities/air_purifier.py). + Its /mode/vs/0 reports modes/supportedModes directly (Smart/Max/Mid/ + WindFree/Sleep) rather than the older family's packed options[] scheme, + which the new air_purifier.FAN capability now binds to a real `fan` + entity (PRESET_MODE, not an ordered speed -- see fan.py). Binds with + zero unbound hrefs.""" + from tests.conftest import _load_device + resources = _load_device('air_purifier_tp1x_da_ac_air') + golden = json.loads((GOLDEN / 'air_purifier_tp1x_da_ac_air.json').read_text()) + state_keys = _new_state_keys('air_purifier_tp1x_da_ac_air', resources) + assert set(state_keys) == set(golden['state_keys']), ( + f"state_keys mismatch:\n" + f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n" + f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}" + ) + + +def test_registry_reproduces_golden_state_keys_for_vacuum_station(): + """A-VSKR-TP1-22-VS9500AL stick-vacuum clean/auto-empty station (issue + #131) -- reports no oneUiVersion; resolved via the new '-VSKR-' + modelNum fallback onto a new vacuum_station registry (see + capabilities/vacuum_station.py's module docstring for why this needed + a new device type rather than reusing an existing one: the dump has no + vacuum-body state at all, only station-specific dustbag/dustbin/ + UV-sanitize resources that share no hrefs with anything else already + modeled). Binds with zero unbound hrefs.""" + from tests.conftest import _load_device + resources = _load_device('vacuum_station') + golden = json.loads((GOLDEN / 'vacuum_station.json').read_text()) + state_keys = _new_state_keys('vacuum_station', resources) + assert set(state_keys) == set(golden['state_keys']), ( + f"state_keys mismatch:\n" + f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n" + f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}" + ) + + def test_resources_from_batch_preferred_over_flat(): from tests.conftest import _resources_from_dump dump = {