Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ee72bebb8 |
@@ -1,6 +1,5 @@
|
|||||||
"""Capabilities for the Samsung ARTIK051_TVTL-class air purifier family
|
"""Capabilities for the Samsung ARTIK051_TVTL-class air purifier family
|
||||||
(model AX60R5080WD/SE, issue #56 -- verified against two independent
|
(model AX60R5080WD/SE, issue #56).
|
||||||
diagnostics dumps of the same internal model).
|
|
||||||
|
|
||||||
Power, kids-lock, remote-control, alarms, and the energy meter are the shared
|
Power, kids-lock, remote-control, alarms, and the energy meter are the shared
|
||||||
common.py capabilities (this family exposes the standard /power/0+/power/vs/0
|
common.py capabilities (this family exposes the standard /power/0+/power/vs/0
|
||||||
@@ -8,32 +7,39 @@ pair and /alarms/vs/0, /energy/consumption/vs/0). /diagnosis/vs/0 reuses
|
|||||||
dishwasher.DIAGNOSIS -- identical field/write contract
|
dishwasher.DIAGNOSIS -- identical field/write contract
|
||||||
(x.com.samsung.da.diagnosisStart, 'Ready' on both dumps).
|
(x.com.samsung.da.diagnosisStart, 'Ready' on both dumps).
|
||||||
|
|
||||||
Two things are deliberately left as raw, unwritable diagnostic sensors rather
|
/mode/vs/0's x.com.samsung.da.options array packs multiple independent
|
||||||
than modeled as real controls, per the "don't guess" rule:
|
'<Prefix>_<value>' flags into one list -- the same packed-list/RMW contract
|
||||||
|
laundry.py's option_value/replace_in_options already model for
|
||||||
|
/course/vs/0's options[] (reused directly below, just against this family's
|
||||||
|
own href). Per issue #56's follow-up (five diagnostics dumps captured with
|
||||||
|
the physical unit set to Auto/Sleep/Low/Medium/High):
|
||||||
|
Light_On / Light_Off -- a plain on/off flag; MODE below models it as a
|
||||||
|
real switch, RMW-replacing just that one entry.
|
||||||
|
Comode_Off -- read 'Off' on *every* one of the five dumps,
|
||||||
|
including High/Low/Medium/Auto -- confirms this
|
||||||
|
is NOT the fan-speed selector (ruling out the
|
||||||
|
original guess); exposed read-only since its
|
||||||
|
actual purpose is still unconfirmed.
|
||||||
|
OptionCode_60282 -- confirmed opaque/not user-facing in the
|
||||||
|
SmartThings app; not modeled (same treatment as
|
||||||
|
range_hood's OptionCode_* token on the same
|
||||||
|
href).
|
||||||
|
Blooming_* -- confirmed to have no corresponding SmartThings
|
||||||
|
app setting; dropped entirely rather than kept
|
||||||
|
as an unexplained diagnostic (it did track 1:1
|
||||||
|
with Sleep mode across the five dumps -- 0 in
|
||||||
|
Sleep, 6 otherwise -- so it's plausibly an
|
||||||
|
automatic side effect of sleep mode, e.g. a
|
||||||
|
display-dimming level, but that's still a guess).
|
||||||
|
|
||||||
/airflow/0, /airflow/vs/0 -- OCF-standard + vendor pair for fan speed/
|
/airflow/0 and /airflow/vs/0's `speed` still isn't modeled as a real
|
||||||
direction, both zeroed/'Off' on every dump seen (device was off in both).
|
fan-speed control: across the same five dumps it read 0 for both Auto *and*
|
||||||
No supportedSpeed/supportedModes list is present anywhere in either dump
|
High, and 3 for Low/Medium/*and* Sleep -- not a monotonic mapping to any
|
||||||
to confirm the valid range, so a write-capable fan/select isn't safe to
|
selectable level, and the dumps were all captured within about three minutes
|
||||||
build yet -- see the issue #56 request for a running-state dump.
|
of each other (only one poll cycle apart at this integration's 30s summary
|
||||||
|
interval), so the values may not have settled after each change before the
|
||||||
/mode/vs/0's x.com.samsung.da.options array packs multiple independent
|
diagnostics snapshot was taken. Exposed read-only pending a confirmed,
|
||||||
'<Prefix>_<value>' flags into one list -- the same packed-list/RMW
|
stable capture -- see the issue #56 discussion for what's needed.
|
||||||
contract laundry.py's option_value/replace_in_options already model for
|
|
||||||
/course/vs/0's options[] (reused directly below, just against this
|
|
||||||
family's own href). Of the tokens seen:
|
|
||||||
Light_On / Light_Off -- read as a plain on/off flag; MODE below
|
|
||||||
models it as a real switch, RMW-
|
|
||||||
replacing just that one list entry.
|
|
||||||
Comode_Off -- never seen non-'Off' on these dumps;
|
|
||||||
likely the fan operating mode the issue
|
|
||||||
describes (Auto/Sleep/1/2/3), but
|
|
||||||
unconfirmed -- exposed read-only.
|
|
||||||
Blooming_0 / Blooming_6 -- meaning unconfirmed; exposed read-only.
|
|
||||||
OptionCode_60282 -- opaque, unchanged across both dumps;
|
|
||||||
not modeled (same treatment as
|
|
||||||
range_hood's OptionCode_* token on the
|
|
||||||
same href).
|
|
||||||
"""
|
"""
|
||||||
from ..capability import Capability
|
from ..capability import Capability
|
||||||
from ..entities import BinarySensorDesc, SensorDesc, SwitchDesc
|
from ..entities import BinarySensorDesc, SensorDesc, SwitchDesc
|
||||||
@@ -68,16 +74,18 @@ def _consumable_state(items, name):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# FilterProgress is a raw 0-100 percentage in both dumps (100 and 62); which
|
# FilterProgress is a 0-100 percentage counting down as the filter wears --
|
||||||
# end of that scale means "replace me" isn't confirmed from the dump alone,
|
# confirmed via issue #56: the SmartThings app shows "Filter needs changing"
|
||||||
# so the entity is named after the raw field rather than asserting a
|
# once this reaches the low end, so 100 means a fresh filter, not "100% worn".
|
||||||
# direction (see issue #56 follow-up questions).
|
# Named 'Filter life' (matching the direction) rather than the 'Filter usage'
|
||||||
|
# convention used elsewhere in this codebase (AC/range_hood's filterUsage
|
||||||
|
# counts up instead), so the two aren't confused.
|
||||||
FILTER = Capability(
|
FILTER = Capability(
|
||||||
href='/consumable/vs/0',
|
href='/consumable/vs/0',
|
||||||
poll_tier='cold',
|
poll_tier='cold',
|
||||||
entities=(
|
entities=(
|
||||||
SensorDesc(key='filter_progress', field='x.com.samsung.da.items',
|
SensorDesc(key='filter_life', field='x.com.samsung.da.items',
|
||||||
name='Filter progress', unit='%', state_class='measurement',
|
name='Filter life', unit='%', state_class='measurement',
|
||||||
icon='mdi:air-filter', entity_category='diagnostic',
|
icon='mdi:air-filter', entity_category='diagnostic',
|
||||||
value_fn=lambda items: int_or_none(
|
value_fn=lambda items: int_or_none(
|
||||||
_consumable_state(items, 'FilterProgress'))),
|
_consumable_state(items, 'FilterProgress'))),
|
||||||
@@ -142,22 +150,20 @@ MODE = Capability(
|
|||||||
rep_fn=bool_option_value('Light'),
|
rep_fn=bool_option_value('Light'),
|
||||||
exists_fn=bool_option_exists('Light'),
|
exists_fn=bool_option_exists('Light'),
|
||||||
write_fn=_light_write),
|
write_fn=_light_write),
|
||||||
# Read-only pending issue #56 follow-up -- see module docstring.
|
# Read-only -- confirmed NOT the fan-speed selector (see module
|
||||||
|
# docstring), actual purpose still unconfirmed.
|
||||||
SensorDesc(key='operating_mode', name='Operating mode', icon='mdi:fan',
|
SensorDesc(key='operating_mode', name='Operating mode', icon='mdi:fan',
|
||||||
entity_category='diagnostic',
|
entity_category='diagnostic',
|
||||||
rep_fn=lambda rep: option_value(rep.get('x.com.samsung.da.options'), 'Comode'),
|
rep_fn=lambda rep: option_value(rep.get('x.com.samsung.da.options'), 'Comode'),
|
||||||
exists_fn=bool_option_exists('Comode')),
|
exists_fn=bool_option_exists('Comode')),
|
||||||
SensorDesc(key='blooming_level', name='Blooming level', icon='mdi:flower',
|
|
||||||
entity_category='diagnostic',
|
|
||||||
rep_fn=lambda rep: option_value(rep.get('x.com.samsung.da.options'), 'Blooming'),
|
|
||||||
exists_fn=bool_option_exists('Blooming')),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# /humidity/0 and /humidity/vs/0 are empty {} on both dumps this family has
|
# /humidity/0 and /humidity/vs/0 are empty {} on every dump seen, and issue
|
||||||
# been verified against -- covered here (not globally, per ignored.py's
|
# #56 confirms this model has no humidity sensor at all (dust/odor only) --
|
||||||
# module docstring) since those hrefs collide with fridge/AC schemas
|
# covered here (not globally, per ignored.py's module docstring) since those
|
||||||
# elsewhere. Same two hrefs and reasoning as airconditioner.py's _AC_IGNORED.
|
# hrefs collide with fridge/AC schemas elsewhere. Same two hrefs and
|
||||||
|
# reasoning as airconditioner.py's _AC_IGNORED.
|
||||||
COVERAGE = [
|
COVERAGE = [
|
||||||
Capability(href='/humidity/0'),
|
Capability(href='/humidity/0'),
|
||||||
Capability(href='/humidity/vs/0'),
|
Capability(href='/humidity/vs/0'),
|
||||||
|
|||||||
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"state_keys": [
|
"state_keys": [
|
||||||
"alarm_code",
|
"alarm_code",
|
||||||
"blooming_level",
|
|
||||||
"clean_level",
|
"clean_level",
|
||||||
"device_active",
|
"device_active",
|
||||||
"diagnosis_status",
|
"diagnosis_status",
|
||||||
@@ -13,7 +12,7 @@
|
|||||||
"energy_this_month_kwh",
|
"energy_this_month_kwh",
|
||||||
"fan_direction",
|
"fan_direction",
|
||||||
"fan_speed_level",
|
"fan_speed_level",
|
||||||
"filter_progress",
|
"filter_life",
|
||||||
"fine_dust",
|
"fine_dust",
|
||||||
"odor",
|
"odor",
|
||||||
"operating_mode",
|
"operating_mode",
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ def test_expected_entities_present():
|
|||||||
state = _state()
|
state = _state()
|
||||||
for key in (
|
for key in (
|
||||||
'power_switch', 'alarm_code', 'dust', 'fine_dust', 'super_fine_dust',
|
'power_switch', 'alarm_code', 'dust', 'fine_dust', 'super_fine_dust',
|
||||||
'odor', 'clean_level', 'filter_progress', 'device_active',
|
'odor', 'clean_level', 'filter_life', 'device_active',
|
||||||
'diagnosis_status', 'fan_speed_level', 'fan_direction',
|
'diagnosis_status', 'fan_speed_level', 'fan_direction',
|
||||||
'display_light', 'operating_mode', 'blooming_level',
|
'display_light', 'operating_mode',
|
||||||
):
|
):
|
||||||
assert key in state, key
|
assert key in state, key
|
||||||
|
|
||||||
@@ -58,8 +58,10 @@ def test_air_quality_sensor_values():
|
|||||||
assert state['clean_level'] == 0
|
assert state['clean_level'] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_filter_progress_reads_named_consumable_item():
|
def test_filter_life_reads_named_consumable_item():
|
||||||
assert _state()['filter_progress'] == 100
|
"""FilterProgress is confirmed (issue #56) to count down as the filter
|
||||||
|
wears -- 100 means fresh, not "100% used"."""
|
||||||
|
assert _state()['filter_life'] == 100
|
||||||
|
|
||||||
|
|
||||||
def test_diagnosis_reuses_dishwasher_capability():
|
def test_diagnosis_reuses_dishwasher_capability():
|
||||||
@@ -76,29 +78,36 @@ def test_light_switch_write_contract():
|
|||||||
the other flags and the list order untouched."""
|
the other flags and the list order untouched."""
|
||||||
desc = next(e for e in air_purifier.MODE.entities if e.key == 'display_light')
|
desc = next(e for e in air_purifier.MODE.entities if e.key == 'display_light')
|
||||||
rep = {'x.com.samsung.da.options': [
|
rep = {'x.com.samsung.da.options': [
|
||||||
'Comode_Off', 'Blooming_0', 'Light_On', 'OptionCode_60282',
|
'Comode_Off', 'Light_On', 'OptionCode_60282',
|
||||||
]}
|
]}
|
||||||
assert desc.rep_fn(rep) is True
|
assert desc.rep_fn(rep) is True
|
||||||
assert desc.write_fn('Off', rep) == (
|
assert desc.write_fn('Off', rep) == (
|
||||||
['mode', 'vs', '0'],
|
['mode', 'vs', '0'],
|
||||||
{'x.com.samsung.da.options': [
|
{'x.com.samsung.da.options': [
|
||||||
'Comode_Off', 'Blooming_0', 'Light_Off', 'OptionCode_60282',
|
'Comode_Off', 'Light_Off', 'OptionCode_60282',
|
||||||
]},
|
]},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_mode_tokens_are_read_only_diagnostics():
|
def test_operating_mode_is_a_read_only_diagnostic():
|
||||||
"""Comode_/Blooming_ tokens surface as raw diagnostic sensors rather than
|
"""Comode_* surfaces as a raw diagnostic sensor rather than a select/
|
||||||
a select/control -- their valid value ranges aren't confirmed yet (see
|
control -- issue #56's five running-state dumps confirmed it reads 'Off'
|
||||||
the air_purifier.py module docstring and the issue #56 follow-up)."""
|
regardless of the device's actual fan setting, ruling out the original
|
||||||
|
guess that it was the fan-speed selector; its real purpose is still
|
||||||
|
unconfirmed (see the air_purifier.py module docstring)."""
|
||||||
operating_mode = next(e for e in air_purifier.MODE.entities if e.key == 'operating_mode')
|
operating_mode = next(e for e in air_purifier.MODE.entities if e.key == 'operating_mode')
|
||||||
blooming = next(e for e in air_purifier.MODE.entities if e.key == 'blooming_level')
|
rep = {'x.com.samsung.da.options': ['Comode_Off']}
|
||||||
rep = {'x.com.samsung.da.options': ['Comode_Off', 'Blooming_6']}
|
|
||||||
assert operating_mode.rep_fn(rep) == 'Off'
|
assert operating_mode.rep_fn(rep) == 'Off'
|
||||||
assert blooming.rep_fn(rep) == '6'
|
|
||||||
assert not hasattr(operating_mode, 'write_fn')
|
assert not hasattr(operating_mode, 'write_fn')
|
||||||
|
|
||||||
|
|
||||||
|
def test_blooming_not_modeled():
|
||||||
|
"""Confirmed (issue #56) to have no corresponding SmartThings app
|
||||||
|
setting -- dropped entirely rather than kept as an unexplained
|
||||||
|
diagnostic."""
|
||||||
|
assert not any(e.key == 'blooming_level' for e in air_purifier.MODE.entities)
|
||||||
|
|
||||||
|
|
||||||
def test_airflow_vs_fallback_only_binds_without_generic():
|
def test_airflow_vs_fallback_only_binds_without_generic():
|
||||||
"""/airflow/vs/0 is a match_fn fallback -- it must not bind when the
|
"""/airflow/vs/0 is a match_fn fallback -- it must not bind when the
|
||||||
OCF-standard /airflow/0 is also present (both are on every dump seen)."""
|
OCF-standard /airflow/0 is also present (both are on every dump seen)."""
|
||||||
|
|||||||
Reference in New Issue
Block a user