From 1bbecfa5c3342a9b5cf1e660dd4a3df9d1fdd2d6 Mon Sep 17 00:00:00 2001 From: blka Date: Tue, 28 Jul 2026 10:07:39 +0200 Subject: [PATCH] Drop /information/vs/0 version entities per review mbillow's follow-up review: exposing read-only Software/Firmware (and Outdoor/Touch IC) version strings is "data for the sake of exposing it" -- no user control, just clutter, and every other registry leaves /information/vs/0 in the global ignore as identity plumbing. Conforming to that stance rather than expanding the entity surface for no user story. - by_type/airconditioner.py: revert to *ignored.IGNORED (no _IGNORED_LESS_INFO filter); drop airconditioner.INFO from the registry. - capabilities/airconditioner.py: remove the INFO capability, the _info_version /_info_items_of_type /_has_info_version helpers, and HREF_INFORMATION. - translations/{en,nl}.json: drop software_version, firmware_version, firmware_version_2/3, outdoor_unit_version, touch_ic_version. - tests: drop the six INFO tests; golden regenerated for 8 AC + dehumidifier. 625 tests pass. --- .../registry/by_type/airconditioner.py | 10 +-- .../registry/capabilities/airconditioner.py | 60 ------------- .../localthings/translations/en.json | 21 ----- .../localthings/translations/nl.json | 21 ----- tests/fixtures/golden/airconditioner.json | 3 - .../golden/airconditioner_ara_ww_tp1_22.json | 3 - .../golden/airconditioner_caww_tp2.json | 3 - .../airconditioner_tp1x_da_ac_rac_01011.json | 4 - .../golden/airconditioner_tp1x_rac.json | 4 - .../airconditioner_tp1x_rac_coolonly.json | 3 - .../golden/airconditioner_tp2x_rac_20k.json | 2 - .../golden/airconditioner_windfree.json | 3 - .../airconditioner_windfree_oscillation.json | 4 - .../golden/airconditioner_window_ac.json | 6 +- tests/test_airconditioner_capabilities.py | 87 ------------------- 15 files changed, 2 insertions(+), 232 deletions(-) diff --git a/custom_components/localthings/registry/by_type/airconditioner.py b/custom_components/localthings/registry/by_type/airconditioner.py index 222221f..94e4797 100644 --- a/custom_components/localthings/registry/by_type/airconditioner.py +++ b/custom_components/localthings/registry/by_type/airconditioner.py @@ -12,17 +12,10 @@ Reuses dishwasher.DIAGNOSIS for /diagnosis/vs/0. from ..capabilities import airconditioner, common, dishwasher, ignored from ._base import DeviceRegistry, _build -# /information/vs/0 is globally ignored (serial/model identity plumbing), but -# the AC exposes Software/Firmware version in its items[] -- model those here -# and drop the no-entity coverage entry so INFO is the sole cap on the href. -# Renamed from the capabilities module's _AC_IGNORED (a list of href strings) -# to avoid the two-meaning collision noted in review. -_IGNORED_LESS_INFO = [c for c in ignored.IGNORED if c.href != '/information/vs/0'] - REGISTRY = DeviceRegistry( name='airconditioner', capabilities=_build([ - *_IGNORED_LESS_INFO, + *ignored.IGNORED, *common.UNIVERSAL, dishwasher.DIAGNOSIS, airconditioner.CLIMATE, @@ -30,7 +23,6 @@ REGISTRY = DeviceRegistry( airconditioner.AUTO_CLEAN, airconditioner.AIR_FILTER, airconditioner.AIR_QUALITY, - airconditioner.INFO, airconditioner.DISPLAY_LIGHT, airconditioner.MUTE_ONCE, airconditioner.CURRENT_LIMIT, diff --git a/custom_components/localthings/registry/capabilities/airconditioner.py b/custom_components/localthings/registry/capabilities/airconditioner.py index 470d723..4355155 100644 --- a/custom_components/localthings/registry/capabilities/airconditioner.py +++ b/custom_components/localthings/registry/capabilities/airconditioner.py @@ -86,29 +86,6 @@ def _tropical_night_write(value, rep, href=None): } -def _info_items_of_type(items, type_): - return [it for it in (items or []) - if isinstance(it, dict) and it.get('x.com.samsung.da.type') == type_] - - -def _info_version(items, type_, ordinal): - """x.com.samsung.da.number of the ordinal-th /information/vs/0 item of the - given type. Boards carry one Software item but 1-3 Firmware items (separate - MCUs), plus an Outdoor unit and (window AC) a Touch IC item -- each a - distinct version string, so they're exposed per-item rather than collapsed - to a single 'first wins' value. None when the ordinal item is absent or - carries no number (e.g. tp2x_rac_20k's second Firmware item).""" - items = _info_items_of_type(items, type_) - if ordinal < len(items): - return items[ordinal].get('x.com.samsung.da.number') - return None - - -def _has_info_version(type_, ordinal): - return lambda rep, resources: _info_version( - rep.get('x.com.samsung.da.items'), type_, ordinal) is not None - - def _filter_unit(rep): """Unit of the filter-usage fields, normalised from filterCapacityUnit ('Hour' -> 'h'). Wired through unit_fn so a board advertising a different @@ -178,7 +155,6 @@ HREF_WIND_DIRECTION = '/wind/direction/vs/0' # swing_mode HREF_WIND_OSCILLATION = '/wind/oscillation/vs/0' # swing_mode fallback HREF_CONVENIENT = '/mode/convenient/vs/0' # preset_mode HREF_TEMPS_VS = '/temperatures/vs/0' # vendor temp fallback (items[] array) -HREF_INFORMATION = '/information/vs/0' # model/serial + Software/Firmware version items CLIMATE_CONSUMED_HREFS = [ HREF_POWER, HREF_POWER_VS, HREF_TEMP_CURRENT, HREF_TEMP_DESIRED, @@ -571,42 +547,6 @@ AIR_QUALITY = Capability( ), ) -# Software/Firmware version from /information/vs/0 items[] (the href is -# globally ignored as identity plumbing; the AC registry drops that entry so -# INFO is the sole cap on it). Boards carry one Software item but 1-3 Firmware -# items (separate MCUs), plus an Outdoor unit and (window AC) a Touch IC item -# -- each a distinct version string, exposed per-item (see _info_version) -# rather than collapsed to a single 'first wins' value. Diagnostic, read-only. -INFO = Capability( - href=HREF_INFORMATION, - poll_tier='cold', - entities=( - SensorDesc(key='software_version', field='x.com.samsung.da.items', - icon='mdi:package-variant', entity_category='diagnostic', - exists_fn=_has_info_version('Software', 0), - value_fn=lambda items: _info_version(items, 'Software', 0)), - SensorDesc(key='firmware_version', field='x.com.samsung.da.items', - icon='mdi:chip', entity_category='diagnostic', - exists_fn=_has_info_version('Firmware', 0), - value_fn=lambda items: _info_version(items, 'Firmware', 0)), - SensorDesc(key='firmware_version_2', field='x.com.samsung.da.items', - icon='mdi:chip', entity_category='diagnostic', - exists_fn=_has_info_version('Firmware', 1), - value_fn=lambda items: _info_version(items, 'Firmware', 1)), - SensorDesc(key='firmware_version_3', field='x.com.samsung.da.items', - icon='mdi:chip', entity_category='diagnostic', - exists_fn=_has_info_version('Firmware', 2), - value_fn=lambda items: _info_version(items, 'Firmware', 2)), - SensorDesc(key='outdoor_unit_version', field='x.com.samsung.da.items', - icon='mdi:fan', entity_category='diagnostic', - exists_fn=_has_info_version('Outdoor', 0), - value_fn=lambda items: _info_version(items, 'Outdoor', 0)), - SensorDesc(key='touch_ic_version', field='x.com.samsung.da.items', - icon='mdi:gesture-tap', entity_category='diagnostic', - exists_fn=_has_info_version('Touch IC', 0), - value_fn=lambda items: _info_version(items, 'Touch IC', 0)), - ), -) # --------------------------------------------------------------------------- # AC-scoped coverage: the CLIMATE_CONSUMED_HREFS above (read by the climate diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index af5e72c..4c8cf80 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -522,24 +522,6 @@ "air_filter_usage_hours": { "name": "Filter usage hours" }, - "software_version": { - "name": "Software version" - }, - "firmware_version": { - "name": "Firmware version" - }, - "firmware_version_2": { - "name": "Firmware version (secondary)" - }, - "firmware_version_3": { - "name": "Firmware version (tertiary)" - }, - "outdoor_unit_version": { - "name": "Outdoor unit version" - }, - "touch_ic_version": { - "name": "Touch IC version" - }, "air_sensing_state": { "name": "Air sensing state" }, @@ -695,9 +677,6 @@ "hood_filter_usage": { "name": "Filter usage" }, - "humidity": { - "name": "Humidity" - }, "instance_temperature": { "name": "{instance_name} temperature" }, diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index 15ba9fd..b74eb7d 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -522,24 +522,6 @@ "air_filter_usage_hours": { "name": "Filterverbruik (uren)" }, - "software_version": { - "name": "Softwareversie" - }, - "firmware_version": { - "name": "Firmwareversie" - }, - "firmware_version_2": { - "name": "Firmwareversie (secundair)" - }, - "firmware_version_3": { - "name": "Firmwareversie (tertiair)" - }, - "outdoor_unit_version": { - "name": "Versie buiteneenheid" - }, - "touch_ic_version": { - "name": "Touch-IC-versie" - }, "air_sensing_state": { "name": "Status luchtmeting" }, @@ -695,9 +677,6 @@ "hood_filter_usage": { "name": "Filterverbruik" }, - "humidity": { - "name": "Luchtvochtigheid" - }, "instance_temperature": { "name": "Temperatuur {instance_name}" }, diff --git a/tests/fixtures/golden/airconditioner.json b/tests/fixtures/golden/airconditioner.json index 42360a8..ffb4c56 100644 --- a/tests/fixtures/golden/airconditioner.json +++ b/tests/fixtures/golden/airconditioner.json @@ -16,12 +16,9 @@ "energy_kwh", "energy_saved_kwh", "fine_dust", - "firmware_version", "humidity", "odor", - "outdoor_unit_version", "power_watts", - "software_version", "super_fine_dust", "tropical_night_mode" ] diff --git a/tests/fixtures/golden/airconditioner_ara_ww_tp1_22.json b/tests/fixtures/golden/airconditioner_ara_ww_tp1_22.json index 6997876..a138056 100644 --- a/tests/fixtures/golden/airconditioner_ara_ww_tp1_22.json +++ b/tests/fixtures/golden/airconditioner_ara_ww_tp1_22.json @@ -12,12 +12,9 @@ "energy_kwh", "energy_saved_kwh", "firmware_update", - "firmware_version", - "firmware_version_2", "humidity", "mute_once", "power_watts", - "software_version", "tropical_night_mode" ] } diff --git a/tests/fixtures/golden/airconditioner_caww_tp2.json b/tests/fixtures/golden/airconditioner_caww_tp2.json index 983ee35..dc60454 100644 --- a/tests/fixtures/golden/airconditioner_caww_tp2.json +++ b/tests/fixtures/golden/airconditioner_caww_tp2.json @@ -13,12 +13,9 @@ "energy_kwh", "energy_saved_kwh", "firmware_update", - "firmware_version", "humidity", "mute_once", - "outdoor_unit_version", "power_watts", - "software_version", "tropical_night_mode" ] } diff --git a/tests/fixtures/golden/airconditioner_tp1x_da_ac_rac_01011.json b/tests/fixtures/golden/airconditioner_tp1x_da_ac_rac_01011.json index a8e81e3..0fb30d7 100644 --- a/tests/fixtures/golden/airconditioner_tp1x_da_ac_rac_01011.json +++ b/tests/fixtures/golden/airconditioner_tp1x_da_ac_rac_01011.json @@ -16,15 +16,11 @@ "energy_saved_kwh", "fine_dust", "firmware_update", - "firmware_version", - "firmware_version_2", - "firmware_version_3", "humidity", "mute_once", "selfcheck_error", "selfcheck_result", "selfcheck_status", - "software_version", "super_fine_dust", "tropical_night_mode" ] diff --git a/tests/fixtures/golden/airconditioner_tp1x_rac.json b/tests/fixtures/golden/airconditioner_tp1x_rac.json index b5f36df..b46a854 100644 --- a/tests/fixtures/golden/airconditioner_tp1x_rac.json +++ b/tests/fixtures/golden/airconditioner_tp1x_rac.json @@ -16,14 +16,10 @@ "energy_kwh", "energy_saved_kwh", "firmware_update", - "firmware_version", - "firmware_version_2", - "firmware_version_3", "mute_once", "selfcheck_error", "selfcheck_result", "selfcheck_status", - "software_version", "tropical_night_mode" ] } diff --git a/tests/fixtures/golden/airconditioner_tp1x_rac_coolonly.json b/tests/fixtures/golden/airconditioner_tp1x_rac_coolonly.json index 6997876..a138056 100644 --- a/tests/fixtures/golden/airconditioner_tp1x_rac_coolonly.json +++ b/tests/fixtures/golden/airconditioner_tp1x_rac_coolonly.json @@ -12,12 +12,9 @@ "energy_kwh", "energy_saved_kwh", "firmware_update", - "firmware_version", - "firmware_version_2", "humidity", "mute_once", "power_watts", - "software_version", "tropical_night_mode" ] } diff --git a/tests/fixtures/golden/airconditioner_tp2x_rac_20k.json b/tests/fixtures/golden/airconditioner_tp2x_rac_20k.json index 60a6469..cf11345 100644 --- a/tests/fixtures/golden/airconditioner_tp2x_rac_20k.json +++ b/tests/fixtures/golden/airconditioner_tp2x_rac_20k.json @@ -11,11 +11,9 @@ "display_light", "energy_kwh", "firmware_update", - "firmware_version", "humidity", "mute_once", "power_watts", - "software_version", "tropical_night_mode" ] } diff --git a/tests/fixtures/golden/airconditioner_windfree.json b/tests/fixtures/golden/airconditioner_windfree.json index e7446ae..2407a44 100644 --- a/tests/fixtures/golden/airconditioner_windfree.json +++ b/tests/fixtures/golden/airconditioner_windfree.json @@ -15,12 +15,9 @@ "dust", "energy_kwh", "fine_dust", - "firmware_version", "humidity", "odor", - "outdoor_unit_version", "power_watts", - "software_version", "super_fine_dust", "tropical_night_mode" ] diff --git a/tests/fixtures/golden/airconditioner_windfree_oscillation.json b/tests/fixtures/golden/airconditioner_windfree_oscillation.json index a210877..99aa641 100644 --- a/tests/fixtures/golden/airconditioner_windfree_oscillation.json +++ b/tests/fixtures/golden/airconditioner_windfree_oscillation.json @@ -13,9 +13,6 @@ "energy_kwh", "energy_saved_kwh", "firmware_update", - "firmware_version", - "firmware_version_2", - "firmware_version_3", "humidity", "mute_once", "overload_protection_active", @@ -23,7 +20,6 @@ "selfcheck_error", "selfcheck_result", "selfcheck_status", - "software_version", "tropical_night_mode" ] } diff --git a/tests/fixtures/golden/airconditioner_window_ac.json b/tests/fixtures/golden/airconditioner_window_ac.json index 76cb555..904800b 100644 --- a/tests/fixtures/golden/airconditioner_window_ac.json +++ b/tests/fixtures/golden/airconditioner_window_ac.json @@ -12,15 +12,11 @@ "energy_kwh", "energy_saved_kwh", "firmware_update", - "firmware_version", "humidity", "mute_once", - "outdoor_unit_version", "power_watts", "selfcheck_error", "selfcheck_result", - "selfcheck_status", - "software_version", - "touch_ic_version" + "selfcheck_status" ] } diff --git a/tests/test_airconditioner_capabilities.py b/tests/test_airconditioner_capabilities.py index ebddc1f..630ccb2 100644 --- a/tests/test_airconditioner_capabilities.py +++ b/tests/test_airconditioner_capabilities.py @@ -634,90 +634,3 @@ def test_sensor_item_value_picks_first_value(): assert airconditioner._sensor_item_value(items, 'Odor') is None assert airconditioner._sensor_item_value(items, 'Missing') is None assert airconditioner._sensor_item_value(None, 'Dust') is None - - -def test_software_and_firmware_version_from_info_items(): - """/information/vs/0 items[] carry Software/Firmware version strings - (the href is otherwise identity plumbing; the AC registry drops the global - ignore so INFO is the sole cap on it). Each version item is a distinct - MCU; they're exposed per-item rather than collapsed to one 'first wins'.""" - reg, resources = _ac_windfree() - state = flatten( - discover(resources, reg.capabilities, reg.pattern_capabilities), resources) - assert state['software_version'] == '02181A230313' - assert state['firmware_version'] == '20082000,FFFFFFFF' - assert state['outdoor_unit_version'] == '20091600,10000400' - # windfree has a single Firmware item -> no _2/_3 entities. - assert 'firmware_version_2' not in state - assert 'touch_ic_version' not in state - - -def test_info_version_exposes_each_firmware_mcu(): - """tp1x_da_ac_rac_01011 reports three Firmware items (separate MCUs) -- - each surfaces as its own diagnostic, not collapsed to the first.""" - reg, resources = _ac_tp1x() - state = flatten( - discover(resources, reg.capabilities, reg.pattern_capabilities), resources) - assert state['software_version'] == '02762A260401' - assert state['firmware_version'] == '02756C25082500,FFFFFFFFFFFFFF' - assert state['firmware_version_2'] == '02669A24092600,02636A10001200' - assert state['firmware_version_3'] == '02672A10001000,FFFFFFFFFFFFFF' - - -def test_info_version_skips_item_with_no_number(): - """tp2x_rac_20k's second Firmware item carries no x.com.samsung.da.number - -- exists_fn suppresses firmware_version_2 rather than binding a - permanently-unknown entity.""" - reg, resources = _resolve('airconditioner_tp2x_rac_20k') - state = flatten( - discover(resources, reg.capabilities, reg.pattern_capabilities), resources) - assert state['firmware_version'] == '102296A23012700' - assert 'firmware_version_2' not in state - - -def test_info_version_touch_ic_on_window_ac(): - """window_ac additionally reports a Touch IC version item.""" - resources = _load_device('airconditioner_window_ac') - info = resources['/information/vs/0'] - reg = for_device_by_model( - info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description']) - state = flatten( - discover(resources, reg.capabilities, reg.pattern_capabilities), resources) - assert state['software_version'] == '02545A260601' - assert state['firmware_version'] == '02543A24061800,FFFFFFFFFFFFFF' - assert state['outdoor_unit_version'] == '02580A10000100,FFFFFFFFFFFFFF' - assert state['touch_ic_version'] == '02553A23031600,FFFFFFFFFFFFFF' - - -def test_info_version_per_ordinal(): - """_info_version returns the ordinal-th item of a type; None when absent - or (via exists_fn) when that item carries no number.""" - items = [ - {'x.com.samsung.da.type': 'Software', 'x.com.samsung.da.number': 'SW1'}, - {'x.com.samsung.da.type': 'Firmware', 'x.com.samsung.da.number': 'FW1'}, - {'x.com.samsung.da.type': 'Firmware', 'x.com.samsung.da.number': 'FW2'}, - {'x.com.samsung.da.type': 'Firmware'}, # no number - ] - assert airconditioner._info_version(items, 'Software', 0) == 'SW1' - assert airconditioner._info_version(items, 'Firmware', 0) == 'FW1' - assert airconditioner._info_version(items, 'Firmware', 1) == 'FW2' - assert airconditioner._info_version(items, 'Firmware', 2) is None # no number - assert airconditioner._info_version(items, 'Firmware', 3) is None # no such ordinal - assert airconditioner._info_version(items, 'Outdoor', 0) is None - assert airconditioner._info_version(None, 'Software', 0) is None - assert airconditioner._has_info_version('Firmware', 0)( # exists_fn - {'x.com.samsung.da.items': items}, {}) is True - assert airconditioner._has_info_version('Firmware', 2)( # item w/o number - {'x.com.samsung.da.items': items}, {}) is False - - -def test_info_drops_global_ignore_on_information_href(): - """The AC registry drops /information/vs/0 from ignored.IGNORED so INFO is - the sole capability on the href (no no-entity coverage cap duplicates).""" - reg, _ = _ac() - caps = reg.capabilities.get('/information/vs/0') - assert caps, '/information/vs/0' - # INFO is the only cap on the href, and it carries real entities. - assert len(caps) == 1 - assert caps[0] is airconditioner.INFO - assert airconditioner.INFO.entities != ()