Add fan-mode control for TP1X_DA-AC-AIR air purifiers (issue #130)
This newer board family reports fan modes (Smart/Max/Mid/WindFree/Sleep) directly on /mode/vs/0's top-level modes/supportedModes fields, unlike the older ARTIK051_TVTL family this registry already supported, which packs everything into an options[] array with no usable fan-speed selector at all (see the module docstring's Comode_Off finding). Both board generations share the /mode/vs/0 href, so the existing MODE capability and a new FAN capability are discriminated by a match_fn checking for the top-level supportedModes field, rather than adding a new device type. The fan entity only exposes PRESET_MODE, not an ordered percentage -- WindFree/Smart/Sleep are named behaviors, not "faster/slower" positions relative to Max/Mid, matching how the AC family's own named convenient modes are modeled as a preset rather than a speed number. Also picked up the rest of this board's previously-unbound hrefs while in there (display, HEPA filter, panel status, pet-filter mode, sound settings), reusing airconditioner.DISPLAY_LIGHT and airconditioner.MUTE_ONCE for the two hrefs identical to the shared DA-AC- board family, since the "incomplete capability coverage" repair was firing on more than just the fan gap the issue described.
This commit is contained in:
9 files changed
+1002
-12
No files matched your search
@@ -1,4 +1,10 @@
|
||||
"""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
|
||||
|
||||
@@ -14,6 +20,7 @@ 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
|
||||
|
||||
|
||||
@@ -22,6 +29,9 @@ 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 +39,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 +134,66 @@ 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)
|
||||
|
||||
@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, ('power', 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, ('power', 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
|
||||
@@ -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,
|
||||
]),
|
||||
)
|
||||
@@ -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 ..entities import (
|
||||
BinarySensorDesc, FanDesc, NumberDesc, SelectDesc, SensorDesc, SwitchDesc,
|
||||
)
|
||||
from .common import 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,192 @@ MODE = Capability(
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _fan_write(payload, rep, href=None):
|
||||
kind, value = payload
|
||||
if kind == 'power':
|
||||
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=lambda rep: (
|
||||
round(int_or_none(rep.get('x.com.samsung.da.filterUsage')) /
|
||||
int_or_none(rep.get('x.com.samsung.da.filterCapacity')) * 100)
|
||||
if int_or_none(rep.get('x.com.samsung.da.filterUsage')) is not None
|
||||
and int_or_none(rep.get('x.com.samsung.da.filterCapacity'))
|
||||
else None
|
||||
), 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=(
|
||||
SelectDesc(key='sound_mode', field='mode',
|
||||
icon='mdi:volume-high',
|
||||
entity_category='config',
|
||||
options_field='supportedModes',
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['settings', 'sound', 'mode', 'vs', '0'], {'mode': p})),
|
||||
),
|
||||
)
|
||||
|
||||
# Read-only descriptor of which sound output the unit has -- only one value
|
||||
# seen, no alternatives to select between.
|
||||
SOUND_OUTPUT = Capability(
|
||||
href='/settings/sound/output/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
SensorDesc(key='sound_output', field='deviceType',
|
||||
icon='mdi:volume-high', entity_category='diagnostic'),
|
||||
),
|
||||
)
|
||||
|
||||
SOUND_VOLUME = Capability(
|
||||
href='/settings/sound/volume/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
NumberDesc(key='sound_volume', field='level',
|
||||
icon='mdi:volume-medium',
|
||||
entity_category='config',
|
||||
native_min_fn=lambda rep: int_or_none(rep.get('minLevel')) or 0,
|
||||
native_max_fn=lambda rep: int_or_none(rep.get('maxLevel')) or 0,
|
||||
step_fn=lambda rep: int_or_none(rep.get('resolution')) or 1,
|
||||
value_fn=int_or_none,
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
['settings', 'sound', 'volume', 'vs', '0'],
|
||||
{'level': str(int(p))})),
|
||||
),
|
||||
)
|
||||
|
||||
# /humidity/0 and /humidity/vs/0 are empty {} on both dumps this family has
|
||||
# been verified against -- covered here (not globally, per ignored.py's
|
||||
# module docstring) since those hrefs collide with fridge/AC schemas
|
||||
# elsewhere. Same two hrefs and reasoning as airconditioner.py's _AC_IGNORED.
|
||||
#
|
||||
# The next six hrefs (issue #130, TP1X_DA-AC-AIR board) are the exact same
|
||||
# resources, same shapes, same reasoning as airconditioner.py's
|
||||
# _AC_IGNORED on the shared DA-AC- board family -- duplicated here rather
|
||||
# than promoted to the global ignored.py list, since that would require
|
||||
# also removing them from _AC_IGNORED in the same change (a global entry
|
||||
# colliding with a family-local bare Capability on the same href raises in
|
||||
# _build()); left as a possible follow-up DRY cleanup.
|
||||
COVERAGE = [
|
||||
Capability(href='/humidity/0'),
|
||||
Capability(href='/humidity/vs/0'),
|
||||
Capability(href='/airlevelcheck/vs/0'), # periodic air-quality sensing scheduler plumbing
|
||||
Capability(href='/availablecontrolsets/vs/0'), # opaque hex-encoded control-set bitmap
|
||||
Capability(href='/da/softreset/vs/0'), # soft-reset trigger plumbing
|
||||
Capability(href='/keepnormalstate/vs/0'), # internal keep-normal flag
|
||||
Capability(href='/personality/presence/vs/0'), # presence-personalization plumbing (empty here)
|
||||
Capability(href='/reserverulesets/vs/0'), # opaque hex-encoded schedule reservation blob
|
||||
# Do-not-disturb/auto-sleep schedule (visible/startTime/endTime/
|
||||
# useTimeSetting/functionState) -- every field reads its inert default
|
||||
# on the only dump seen (times both '00:00:00', useTimeSetting/
|
||||
# functionState both 'false'). Same "needs a multi-field schedule
|
||||
# editor" treatment as fridge.py's /defrost/reservation/vs/0.
|
||||
Capability(href='/dnd/autosleep/vs/0'),
|
||||
]
|
||||
@@ -16,6 +16,9 @@
|
||||
"cloud_connected": {
|
||||
"name": "Cloud connected"
|
||||
},
|
||||
"dustbag_full": {
|
||||
"name": "Dust bag full"
|
||||
},
|
||||
"cooktop_power": {
|
||||
"name": "Cooktop power"
|
||||
},
|
||||
@@ -131,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"
|
||||
@@ -188,6 +206,13 @@
|
||||
"on": "On"
|
||||
}
|
||||
},
|
||||
"discharging_time": {
|
||||
"name": "Discharging time",
|
||||
"state": {
|
||||
"1": "1 minute",
|
||||
"3": "3 minutes"
|
||||
}
|
||||
},
|
||||
"cycle": {
|
||||
"name": "Cycle"
|
||||
},
|
||||
@@ -518,6 +543,36 @@
|
||||
"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": {
|
||||
@@ -739,6 +794,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"
|
||||
},
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
"cloud_connected": {
|
||||
"name": "Verbonden met cloud"
|
||||
},
|
||||
"dustbag_full": {
|
||||
"name": "Stofzak vol"
|
||||
},
|
||||
"cooktop_power": {
|
||||
"name": "Kookplaat voeding"
|
||||
},
|
||||
@@ -131,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"
|
||||
@@ -188,6 +206,13 @@
|
||||
"on": "Aan"
|
||||
}
|
||||
},
|
||||
"discharging_time": {
|
||||
"name": "Leegtijd",
|
||||
"state": {
|
||||
"1": "1 minuut",
|
||||
"3": "3 minuten"
|
||||
}
|
||||
},
|
||||
"cycle": {
|
||||
"name": "Programma"
|
||||
},
|
||||
@@ -518,6 +543,36 @@
|
||||
"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": {
|
||||
@@ -739,6 +794,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"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,466 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/airlevelcheck/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sensingState": "NonProcessing",
|
||||
"x.com.samsung.da.lastSensingTime": "1785157841",
|
||||
"x.com.samsung.da.lastSensingLevel": "Kr1",
|
||||
"x.com.samsung.da.periodicSensingSkipStatus": "Off",
|
||||
"x.com.samsung.da.periodicSensingSkipTime": "00000000",
|
||||
"x.com.samsung.da.periodicSensingActivationState": "On",
|
||||
"x.com.samsung.da.autoExeState": "Off",
|
||||
"x.com.samsung.da.supportedAutoExeState": [
|
||||
"Off",
|
||||
"Airpurify",
|
||||
"Alarm"
|
||||
],
|
||||
"x.com.samsung.da.startSensingOnce": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-27T13:18:51",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
}
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.alarms"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/availablecontrolsets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "3B000100000A0000000000000000",
|
||||
"x.com.samsung.da.id": "VTL",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "2650000000",
|
||||
"x.com.samsung.da.airconOptionList": [
|
||||
"AI_PURIFY",
|
||||
"WELCOMECARE",
|
||||
"DR"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/da/softreset/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.softwarereset": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/devicespecificinfo/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.deviceActive": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/display/vs/0",
|
||||
"rep": {
|
||||
"mode": "On",
|
||||
"supportedModes": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/dnd/autosleep/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.visible": "true",
|
||||
"x.com.samsung.da.startTime": "00:00:00",
|
||||
"x.com.samsung.da.endTime": "00:00:00",
|
||||
"x.com.samsung.da.useTimeSetting": "false",
|
||||
"x.com.samsung.da.functionState": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.start": "1970-01-01T00:00:00Z",
|
||||
"x.com.samsung.da.override": "Off",
|
||||
"x.com.samsung.da.durationminutes": "0",
|
||||
"x.com.samsung.da.drlcLevel": "0",
|
||||
"x.com.samsung.da.realSaving": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cumulativeDate": "1785158301",
|
||||
"x.com.samsung.da.cumulativeSavedPower": "0",
|
||||
"x.com.samsung.da.cumulativePower": "32428",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePowerType": "total"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+09:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/hepafilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsage": "70",
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterCapacity": "8760",
|
||||
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"replaceable"
|
||||
],
|
||||
"x.com.samsung.da.filterStatus": "normal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.modelNum": "TP1X_DA-AC-AIR-01031_0000|10268241|70000721001813CC4D1A490301082100",
|
||||
"x.com.samsung.da.description": "TP1X_DA-AC-AIR-01031_0000",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02660A260401",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.newVersionAvailable": "0",
|
||||
"x.com.samsung.da.number": "02682A25042102,02683A25031801"
|
||||
}
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "AP2",
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagTsId": "DA01"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/keepnormalstate/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.keepnormal": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.kidsLock": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/light/vs/0",
|
||||
"rep": {
|
||||
"mode": "On",
|
||||
"supportedModes": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": [
|
||||
"Smart"
|
||||
],
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Smart",
|
||||
"Max",
|
||||
"Mid",
|
||||
"WindFree",
|
||||
"Sleep"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"OptionCode_23418",
|
||||
"SmartSleep_Off",
|
||||
"WelcomeCare_On",
|
||||
"Pollution_Off",
|
||||
"ProtectOperation_Off",
|
||||
"UpdateAllow_NotAllowed"
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.mode"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/muteonce/vs/0",
|
||||
"rep": {
|
||||
"muteonce": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/panel/vs/0",
|
||||
"rep": {
|
||||
"status": "Close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/personality/presence/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/petfilteractivation/vs/0",
|
||||
"rep": {
|
||||
"status": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "On",
|
||||
"operationNumber": "36",
|
||||
"displayCondition": "Enable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.longnotisubscription": "true",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/reserverulesets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "3F00000000000000000000",
|
||||
"x.com.samsung.da.id": "VTL",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sensors/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cleanLevel": "1",
|
||||
"x.com.samsung.da.total": "5",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Sensor for CleanLevel",
|
||||
"x.com.samsung.da.type": "CleanLevel",
|
||||
"x.com.samsung.da.value": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Sensor for Odor",
|
||||
"x.com.samsung.da.type": "Odor",
|
||||
"x.com.samsung.da.value": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Sensor for Dust",
|
||||
"x.com.samsung.da.type": "Dust",
|
||||
"x.com.samsung.da.value": [
|
||||
"5",
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "3",
|
||||
"x.com.samsung.da.description": "Sensor for FineDust",
|
||||
"x.com.samsung.da.type": "FineDust",
|
||||
"x.com.samsung.da.value": [
|
||||
"5",
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "4",
|
||||
"x.com.samsung.da.description": "Sensor for SuperFineDust",
|
||||
"x.com.samsung.da.type": "SuperFineDust",
|
||||
"x.com.samsung.da.value": [
|
||||
"5",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.sensors"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.s"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/settings/sound/mode/vs/0",
|
||||
"rep": {
|
||||
"mode": "mute",
|
||||
"supportedModes": [
|
||||
"mute",
|
||||
"buzzer"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/settings/sound/output/vs/0",
|
||||
"rep": {
|
||||
"deviceType": "buzzer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/settings/sound/volume/vs/0",
|
||||
"rep": {
|
||||
"level": "0",
|
||||
"minLevel": "0",
|
||||
"maxLevel": "3",
|
||||
"resolution": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "Micom",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"x.com.samsung.da.newVersionNo": "12250421",
|
||||
"x.com.samsung.da.currentVersionInfo": "10000000",
|
||||
"otnStatus": "None",
|
||||
"flashingProgress": "0",
|
||||
"otnTarget": "main",
|
||||
"otnCompleteDate": "noHistory",
|
||||
"scheduledTime": "None",
|
||||
"swVersionInfo": {
|
||||
"platform": "Tizen Lite",
|
||||
"oneUiVersion": "7.0 Air purifier",
|
||||
"osVersion": "4.0"
|
||||
},
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "AVT-KR-TP1-24-AXXX00",
|
||||
"versions": [
|
||||
"12260401"
|
||||
],
|
||||
"visVersion": "260401"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "895010268241FFFFFFFF",
|
||||
"versions": [
|
||||
"25042102",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "250421"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "09501026824110268341",
|
||||
"versions": [
|
||||
"25042102",
|
||||
"25031801"
|
||||
],
|
||||
"visVersion": "250421"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Seoul",
|
||||
"offset": "+09:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connectionconfig/vs/0",
|
||||
"rep": {
|
||||
"autoReconnectionMinVersion": "1.0",
|
||||
"autoReconnection": "true",
|
||||
"autoReconnectionProtocolType": [
|
||||
"helper_hotspot",
|
||||
"ble_ocf"
|
||||
],
|
||||
"supportedWiFiAuthType": [
|
||||
"OPEN",
|
||||
"WEP",
|
||||
"WPA-PSK",
|
||||
"WPA2-PSK",
|
||||
"SAE"
|
||||
],
|
||||
"supportedWiFiCryptoType": [
|
||||
"TKIP",
|
||||
"AES",
|
||||
"WEP-64",
|
||||
"WEP-128"
|
||||
],
|
||||
"supportedWiFiFreq": [
|
||||
"2.4G"
|
||||
],
|
||||
"calmConnectionCare": {
|
||||
"version": "1.0",
|
||||
"role": [
|
||||
"things"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wirelessinfo/vs/0",
|
||||
"rep": {
|
||||
"macaddressWiFi": "**REDACTED**",
|
||||
"macaddressBLE": "**REDACTED**",
|
||||
"connectedApSsid": "Branden_Summit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/quickcontrol/info/vs/0",
|
||||
"rep": {
|
||||
"supportedVersion": "1.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -588,6 +588,27 @@ def test_registry_reproduces_golden_state_keys_for_oven_mw7300b():
|
||||
)
|
||||
|
||||
|
||||
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_resources_from_batch_preferred_over_flat():
|
||||
from tests.conftest import _resources_from_dump
|
||||
dump = {
|
||||
|
||||
Reference in new issue
Block a user