diff --git a/custom_components/localthings/icons.json b/custom_components/localthings/icons.json new file mode 100644 index 0000000..47061c6 --- /dev/null +++ b/custom_components/localthings/icons.json @@ -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" + } + } + } + } +} 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():