Merge pull request #170 from mbillow/claude/issue-triage-gating-gf6bg4

Fix AC entity gating from #164, add missing preset/mode icons (#166, #169)
This commit is contained in:
Marc Billow
2026-07-28 13:50:53 -05:00
committed by GitHub
5 changed files with 170 additions and 20 deletions
+61
View File
@@ -0,0 +1,61 @@
{
"entity": {
"climate": {
"airconditioner": {
"state_attributes": {
"fan_mode": {
"state": {
"turbo": "mdi:speedometer",
"max": "mdi:speedometer"
}
},
"preset_mode": {
"state": {
"ai_comfort": "mdi:creation",
"quiet": "mdi:volume-off",
"smart": "mdi:brain",
"speed": "mdi:speedometer",
"nano": "mdi:weather-dust",
"nanosleep": "mdi:sleep",
"longwind": "mdi:weather-windy",
"motiondirect": "mdi:account-arrow-right",
"motionindirect": "mdi:account-arrow-left",
"drycomfort": "mdi:water-percent",
"2step": "mdi:stairs"
}
}
}
}
},
"fan": {
"air_purifier_fan": {
"state_attributes": {
"preset_mode": {
"state": {
"smart": "mdi:brain",
"max": "mdi:speedometer",
"mid": "mdi:speedometer-medium",
"windfree": "mdi:weather-dust",
"sleep": "mdi:sleep"
}
}
}
}
},
"sensor": {
"machine_state": {
"state": {
"idle": "mdi:power-standby",
"active": "mdi:play",
"pause": "mdi:pause"
}
},
"connection_mode": {
"state": {
"observe": "mdi:broadcast",
"poll": "mdi:sync"
}
}
}
}
}
@@ -128,7 +128,27 @@ def _sensor_item_value(items, type_):
def _has_sensor_type(type_):
"""Item-type presence AND a corroborating top-level
x.com.samsung.da.cleanLevel scalar on the same /sensors/vs/0 rep.
Item-type presence alone isn't a real capability signal: issue #166
(ARxxTXFCAWKNEU, board ARTIK051_PRAC_20K) lists all five item types with
permanent zero values on both its units, the same shape as this repo's
other ARTIK051_PRAC_20K dumps (the original issue #17 dump and the
windfree fixture -- verified against the *same* board revision, per its
/information/vs/0) -- yet the #166 reporter confirmed none of these
sensors are physically present. The top-level cleanLevel scalar (separate
from the CleanLevel item inside items[]) is only ever present alongside
genuinely populated readings in every dump on record: present on
tp1x_da_ac_rac_01011 (real AC, clean_level=1) and the tp1x_da_ac_air air
purifier fixture (all five types real/nonzero), absent on every
ARTIK051_PRAC_20K dump (all zero, including both #166 units). A small
sample, but a consistent one and the only signal found that actually
explains the #166 report -- gate on it rather than leaving the always-
present item type to imply a capability that may not exist."""
def fn(rep, resources):
if 'x.com.samsung.da.cleanLevel' not in rep:
return False
return any(isinstance(i, dict) and i.get('x.com.samsung.da.type') == type_
for i in (rep.get('x.com.samsung.da.items') or []))
return fn
@@ -441,12 +461,26 @@ CLIMATE = Capability(
# Single-token option_write. Cloud: custom.airConditionerTropicalNightMode.
# Gated off the legacy board for the same reason as beep above -- its
# Sleep_ token is already the good_sleep Number below.
#
# exists_fn only proves the Sleep_ token slot is present, not that
# tropical night mode is a real feature of the unit: issue #166
# (ARxxTXFCAWKNEU) reports Sleep_0 in every dump -- the exact same
# always-there-at-zero shape as the issue #17 dump #164 was verified
# against -- yet the reporter confirmed their remote/app has no
# tropical night mode control at all. Samsung's OCF options[] blob
# carries this scaffolding token regardless of physical capability,
# so there's no reliable signal here to gate on (same 'don't guess'
# rule as elsewhere in this file, just with no signal to guess from).
# Registered but disabled by default, same precedent as
# fridge.rack_count / cooktop.paired_hood_* -- units that do have the
# feature can enable it themselves.
NumberDesc(key='tropical_night_mode', rep_fn=_tropical_night_value,
exists_fn=lambda rep, resources: (
not is_legacy_board(resources)
and _option_token(rep, 'Sleep') is not None),
write_fn=_tropical_night_write,
native_min=0, native_max=16, step=1,
enabled_default=False,
icon='mdi:weather-night', entity_category='config'),
# Settings that this board generation keeps as options[] tokens.
SwitchDesc(key='spi', rep_fn=_option_token_on('Spi'),
@@ -702,6 +736,21 @@ HUMIDITY = Capability(
# are 1- or 2-element arrays with no corroborating scalar, so they stay string
# diagnostics (see _sensor_item_value for the 2-element ambiguity and why only
# v[0] is taken). No unit is advertised on the resource, so no device_class.
#
# _has_sensor_type requires that same top-level cleanLevel scalar, not just
# item-type presence: issue #166 (ARxxTXFCAWKNEU, board ARTIK051_PRAC_20K)
# reports all five item types on both its units, values permanently
# '0'/['0','0'] -- the exact same shape as the issue #17 dump AIR_QUALITY was
# first verified against and the WindFree fixture (see
# test_air_quality_sensors_from_sensors_vs_items) -- both the *same board
# revision* per /information/vs/0, so that "verification" never actually
# proved a real sensor either. Item-type presence alone is Samsung's OCF
# scaffolding listing every known sensor type regardless of physical
# capability, not a capability signal. The top-level scalar is: it's present,
# with genuinely populated readings, on every dump with a confirmed-real
# sensor (tp1x_da_ac_rac_01011, and the tp1x_da_ac_air air purifier fixture),
# and absent on every all-zero ARTIK051_PRAC_20K dump on record, including
# both #166 units. Gate on it.
AIR_QUALITY = Capability(
href='/sensors/vs/0',
poll_tier='cold',
-5
View File
@@ -7,19 +7,14 @@
"alarm_code",
"auto_clean",
"beep",
"clean_level",
"climate",
"current_temperature_c",
"diagnosis_status",
"display_light",
"dust",
"energy_kwh",
"energy_saved_kwh",
"fine_dust",
"humidity",
"odor",
"power_watts",
"super_fine_dust",
"tropical_night_mode"
]
}
-5
View File
@@ -7,18 +7,13 @@
"alarm_code",
"auto_clean",
"beep",
"clean_level",
"climate",
"current_temperature_c",
"diagnosis_status",
"display_light",
"dust",
"energy_kwh",
"fine_dust",
"humidity",
"odor",
"power_watts",
"super_fine_dust",
"tropical_night_mode"
]
}
+60 -10
View File
@@ -564,6 +564,19 @@ def test_tropical_night_write_is_single_token_options_merge():
['mode', 'vs', '0'], {'x.com.samsung.da.options': ['Sleep_6']})
def test_tropical_night_disabled_by_default():
"""Issue #166: Sleep_0 is present in /mode/vs/0's options on every dump
seen, including the original issue #17 dump this capability was verified
against (see airconditioner_device.json / _ac()) -- yet the #166 reporter
confirmed their unit (also an ARTIK051_PRAC_20K board, per its own
/information/vs/0) has no tropical night mode feature at all. The token
slot's presence proves nothing about the physical feature, so this can't
be existence-gated any tighter than it already is -- registered but
disabled by default instead, same precedent as fridge.rack_count /
cooktop.paired_hood_model."""
assert _tropical_desc().enabled_default is False
def test_tropical_night_absent_when_no_sleep_token():
"""TP1X_DA-AC-WAC (window AC) carries no Sleep_ option -- tropical night
mode must not bind."""
@@ -661,19 +674,56 @@ def test_air_quality_sensors_from_sensors_vs_items():
by a top-level cleanLevel scalar, so it's an int measurement; the others
are string diagnostics. Dust/FineDust/SuperFineDust carry a 2-element
array whose second element is unconfirmed -- v[0] is taken as the reading
(see _sensor_item_value)."""
(see _sensor_item_value). Exercised on tp1x_da_ac_rac_01011, the fixture
proven real by the corroborating scalar (see
test_air_quality_present_with_corroborating_clean_level_scalar) --
windfree no longer applies here post-#166 fix, since it lacks that
scalar and binds no air-quality entities at all."""
reg, resources = _ac_tp1x()
state = flatten(
discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
assert state['clean_level'] == 1 # numeric (int), corroborated
for key in ('dust', 'fine_dust', 'super_fine_dust'):
assert state[key] == '0' # string diagnostic
def test_air_quality_absent_without_corroborating_clean_level_scalar():
"""Issue #166 (ARxxTXFCAWKNEU, board ARTIK051_PRAC_20K): /sensors/vs/0
lists all five item types, values permanently '0'/['0', '0'] -- the exact
same shape as the WindFree fixture, which is the *same board revision*
(see /information/vs/0: both report modelNum
'ARTIK051_PRAC_20K|10217841|...') that this capability was originally
verified against. The reporter confirmed none of these sensors are
physically present on their unit, which means that original
"verification" never actually proved a real sensor either -- item-type
presence is Samsung's OCF scaffolding, not a capability signal (see
_has_sensor_type). The tell that's actually reliable: a top-level
x.com.samsung.da.cleanLevel scalar (separate from the CleanLevel item),
present only alongside genuinely populated readings on every dump on
record. WindFree lacks it -- so post-fix, none of the five entities
should bind there at all, matching the #166 report."""
reg, resources = _ac_windfree()
state = flatten(
discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
assert state['clean_level'] == 0 # numeric (int), corroborated
for key in ('odor', 'dust', 'fine_dust', 'super_fine_dust'):
assert state[key] == '0' # string diagnostic
# tp1x_da_ac_rac_01011 is the only fixture with a non-zero air-quality
# reading -- the one that catches a value_fn regression.
reg2, resources2 = _ac_tp1x()
state2 = flatten(
discover(resources2, reg2.capabilities, reg2.pattern_capabilities), resources2)
assert state2['clean_level'] == 1
assert 'x.com.samsung.da.cleanLevel' not in resources['/sensors/vs/0']
for key in ('clean_level', 'odor', 'dust', 'fine_dust', 'super_fine_dust'):
assert key not in state, key
def test_air_quality_present_with_corroborating_clean_level_scalar():
"""tp1x_da_ac_rac_01011 carries the top-level cleanLevel scalar alongside
a genuinely populated CleanLevel reading -- the one fixture in this repo
proven real rather than placeholder, so its air-quality entities must
still bind post-fix. It has no Odor item at all (unrelated to the scalar
gate -- item-type absence, not zero-value ambiguity)."""
reg, resources = _ac_tp1x()
assert resources['/sensors/vs/0']['x.com.samsung.da.cleanLevel'] == '1'
state = flatten(
discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
assert state['clean_level'] == 1
for key in ('dust', 'fine_dust', 'super_fine_dust'):
assert key in state
assert 'odor' not in state
def test_air_quality_absent_when_no_sensor_items():