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.
This commit is contained in:
blka
2026-07-28 10:07:39 +02:00
parent b18500dec3
commit 1bbecfa5c3
15 changed files with 2 additions and 232 deletions
@@ -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,
@@ -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
@@ -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"
},
@@ -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}"
},
-3
View File
@@ -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"
]
@@ -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"
]
}
-3
View File
@@ -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"
]
}
@@ -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"
]
-4
View File
@@ -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"
]
}
@@ -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"
]
}
@@ -11,11 +11,9 @@
"display_light",
"energy_kwh",
"firmware_update",
"firmware_version",
"humidity",
"mute_once",
"power_watts",
"software_version",
"tropical_night_mode"
]
}
-3
View File
@@ -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"
]
@@ -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"
]
}
+1 -5
View File
@@ -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"
]
}
-87
View File
@@ -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 != ()