From 739881de16202d29c3e953ef6878e0bfec54f17f Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Tue, 28 Jul 2026 18:12:02 +0000 Subject: [PATCH 1/4] Gate AC tropical night mode and air-quality sensors on real capability signals (#166) Issue #166 (ARxxTXFCAWKNEU, board ARTIK051_PRAC_20K) reported tropical night mode, clean level, dust, fine dust, odor, and super fine dust entities showing up even though the reporter's units have no such physical features. All six were added in #164. The Sleep_ options token backing tropical_night_mode is present in every AC dump on record regardless of confirmed reality, so there's no usable signal at boot time -- it's now registered but disabled by default (matching the precedent already set by fridge.rack_count / cooktop.paired_hood_model), letting units that do have it opt in. /sensors/vs/0's item-type list has the same problem (all five types always listed, permanently zero on this board), but there turned out to be a real tell: a top-level x.com.samsung.da.cleanLevel scalar is present only alongside genuinely populated readings on every dump on record (tp1x_da_ac_rac_01011, the tp1x_da_ac_air air purifier fixture) and absent on every all-zero ARTIK051_PRAC_20K dump, including both #166 units and the original windfree/#17 fixtures this capability was first verified against -- which, per their /information/vs/0, turn out to be the same board revision as #166's units, so that "verification" never actually proved a real sensor either. AIR_QUALITY's exists_fn now requires that scalar, and the windfree/airconditioner golden fixtures are updated to match (those five entities no longer bind there). --- .../registry/capabilities/airconditioner.py | 49 +++++++++++++ tests/fixtures/golden/airconditioner.json | 5 -- .../golden/airconditioner_windfree.json | 5 -- tests/test_airconditioner_capabilities.py | 70 ++++++++++++++++--- 4 files changed, 109 insertions(+), 20 deletions(-) diff --git a/custom_components/localthings/registry/capabilities/airconditioner.py b/custom_components/localthings/registry/capabilities/airconditioner.py index d2cafa3..ac159d8 100644 --- a/custom_components/localthings/registry/capabilities/airconditioner.py +++ b/custom_components/localthings/registry/capabilities/airconditioner.py @@ -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', diff --git a/tests/fixtures/golden/airconditioner.json b/tests/fixtures/golden/airconditioner.json index ffb4c56..7d07080 100644 --- a/tests/fixtures/golden/airconditioner.json +++ b/tests/fixtures/golden/airconditioner.json @@ -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" ] } diff --git a/tests/fixtures/golden/airconditioner_windfree.json b/tests/fixtures/golden/airconditioner_windfree.json index 2407a44..1b7e382 100644 --- a/tests/fixtures/golden/airconditioner_windfree.json +++ b/tests/fixtures/golden/airconditioner_windfree.json @@ -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" ] } diff --git a/tests/test_airconditioner_capabilities.py b/tests/test_airconditioner_capabilities.py index 282dd23..d556f5b 100644 --- a/tests/test_airconditioner_capabilities.py +++ b/tests/test_airconditioner_capabilities.py @@ -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(): From cfa82e8853c3cac01176c5abc87be0bb67bf53cb Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Tue, 28 Jul 2026 18:20:10 +0000 Subject: [PATCH 2/4] Add icons for AC preset/fan modes not covered by HA's built-ins (#169) HA's core climate component already ships default icons for common preset_mode/fan_mode values (eco, away, sleep, auto, low/medium/high, ...), but this integration's own values -- WindFree (nano/nanosleep), Quiet, Smart, Speed, Long wind, the motion-aware direct/indirect presets, Dry comfort, 2-Step, and the turbo/max fan speeds some boards report -- fall outside that vocabulary and rendered with the generic circle-dot fallback (the icon the #169 screenshot is missing). Adds icons.json with an icon per value, mirroring the state-label catalog these same values already have in translations/en.json. --- custom_components/localthings/icons.json | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 custom_components/localthings/icons.json diff --git a/custom_components/localthings/icons.json b/custom_components/localthings/icons.json new file mode 100644 index 0000000..a8485eb --- /dev/null +++ b/custom_components/localthings/icons.json @@ -0,0 +1,31 @@ +{ + "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-windy-variant", + "nanosleep": "mdi:sleep", + "longwind": "mdi:weather-windy", + "motiondirect": "mdi:account-arrow-right", + "motionindirect": "mdi:account-arrow-left", + "drycomfort": "mdi:water-percent", + "2step": "mdi:stairs" + } + } + } + } + } + } +} From 5f47fc0477e2f001e71f54eb348641e70edfae34 Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Tue, 28 Jul 2026 18:31:31 +0000 Subject: [PATCH 3/4] Add per-state icons for the remaining entities that render with none HA only consults icon-translation state icons when the entity has no static icon of its own (Entity.icon, if set, always wins -- see homeassistant.helpers.entity's state_attributes construction). Audited every entity with a labelled state/state_attributes catalog in translations/en.json against its descriptor's icon= setting: every select (cycles, courses, brightness levels, ...) and most sensors already carry a fixed icon in code, so per-state icons there would be silently shadowed. The three that don't -- air_purifier_fan's preset_mode, machine_state, and connection_mode -- get one per value here. --- custom_components/localthings/icons.json | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/custom_components/localthings/icons.json b/custom_components/localthings/icons.json index a8485eb..13dc586 100644 --- a/custom_components/localthings/icons.json +++ b/custom_components/localthings/icons.json @@ -26,6 +26,36 @@ } } } + }, + "fan": { + "air_purifier_fan": { + "state_attributes": { + "preset_mode": { + "state": { + "smart": "mdi:brain", + "max": "mdi:speedometer", + "mid": "mdi:speedometer-medium", + "windfree": "mdi:weather-windy-variant", + "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" + } + } } } } From 6d185e4e3d356e1e62a7d047b2710abbcdd04ccf Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Tue, 28 Jul 2026 18:47:10 +0000 Subject: [PATCH 4/4] Match WindFree icon to the official smartthings integration's choice HA core's bundled smartthings integration (the cloud counterpart to this same Samsung AC feature set) uses mdi:weather-dust for its wind_free preset rather than a generic windy icon -- a better fit for a feature about avoiding direct airflow, not blowing harder. Match it for both the AC climate preset and the air purifier fan preset. --- custom_components/localthings/icons.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/localthings/icons.json b/custom_components/localthings/icons.json index 13dc586..47061c6 100644 --- a/custom_components/localthings/icons.json +++ b/custom_components/localthings/icons.json @@ -15,7 +15,7 @@ "quiet": "mdi:volume-off", "smart": "mdi:brain", "speed": "mdi:speedometer", - "nano": "mdi:weather-windy-variant", + "nano": "mdi:weather-dust", "nanosleep": "mdi:sleep", "longwind": "mdi:weather-windy", "motiondirect": "mdi:account-arrow-right", @@ -35,7 +35,7 @@ "smart": "mdi:brain", "max": "mdi:speedometer", "mid": "mdi:speedometer-medium", - "windfree": "mdi:weather-windy-variant", + "windfree": "mdi:weather-dust", "sleep": "mdi:sleep" } }