From 1f7bdc9ac6060df10f550baeceb62a15c4d1628c Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Sun, 9 Aug 2026 01:21:17 +0000 Subject: [PATCH] fridge: give DEODOR_FILTER its own entity keys, not AIR_FILTER's (code review) DEODOR_FILTER reused AIR_FILTER.entities verbatim, so both capabilities produced identically-keyed entities (air_filter_usage/air_filter_status) despite living at different hrefs. adapter.flatten()'s key derivation has no href component, so a unit reporting both /filter/airdustfilter/vs/0 and /filter/deodorfilter/vs/0 would silently clobber one filter's reading with the other's -- the exact collision AIR_FILTER's own 'air_' prefix was chosen to avoid against WATER_FILTER's filter_usage/filter_status. Gives DEODOR_FILTER its own deodor_filter_usage/deodor_filter_status keys (status still shares the filter_status translation_key, same as AIR_FILTER already does). Updated the winecellar fixture's golden and test, and added deodor_filter_usage to all seven translation catalogs. --- .../registry/capabilities/fridge.py | 27 ++++++++++++++++++- .../localthings/translations/cs.json | 3 +++ .../localthings/translations/de.json | 3 +++ .../localthings/translations/en.json | 3 +++ .../localthings/translations/es.json | 3 +++ .../localthings/translations/it.json | 3 +++ .../localthings/translations/ko.json | 3 +++ .../localthings/translations/nl.json | 3 +++ .../golden/refrigerator_winecellar.json | 4 +-- ...st_refrigerator_winecellar_capabilities.py | 18 ++++++++----- 10 files changed, 61 insertions(+), 9 deletions(-) diff --git a/custom_components/localthings/registry/capabilities/fridge.py b/custom_components/localthings/registry/capabilities/fridge.py index 91d2e47..ef82886 100644 --- a/custom_components/localthings/registry/capabilities/fridge.py +++ b/custom_components/localthings/registry/capabilities/fridge.py @@ -411,10 +411,35 @@ AUTO_DOOR_VARIANT = Capability( # comment). filterUsage reads '-1' on the only dump seen (filterStatus # 'normal'), relayed as-is rather than special-cased -- no second dump to # confirm whether that's a real sentinel or this unit just not tracking it. +# Own 'deodor_'-prefixed keys rather than reusing AIR_FILTER.entities +# verbatim -- same collision AIR_FILTER's own 'air_' prefix was chosen to +# avoid against WATER_FILTER, and both filters are plausible on one unit +# (this device's own board reports an internal air filter on other +# TP1X_REF_21K variants). DEODOR_FILTER = Capability( href="/filter/deodorfilter/vs/0", poll_tier="cold", - entities=AIR_FILTER.entities, + entities=( + SensorDesc( + key="deodor_filter_usage", + field="x.com.samsung.da.filterUsage", + unit="%", + state_class="measurement", + icon="mdi:air-filter", + entity_category="diagnostic", + value_fn=int_or_none, + ), + SensorDesc( + key="deodor_filter_status", + field="x.com.samsung.da.filterStatus", + device_class="enum", + options=("normal", "wash", "replace"), + translation_key="filter_status", + icon="mdi:air-filter", + entity_category="diagnostic", + value_fn=lambda v: v.lower() if isinstance(v, str) else v, + ), + ), ) # Wine-cellar multi-compartment pantry select (issue #328): same diff --git a/custom_components/localthings/translations/cs.json b/custom_components/localthings/translations/cs.json index b488ae0..977b8db 100644 --- a/custom_components/localthings/translations/cs.json +++ b/custom_components/localthings/translations/cs.json @@ -761,6 +761,9 @@ "air_filter_usage_hours": { "name": "Hodiny využití filtru" }, + "deodor_filter_usage": { + "name": "Využití filtru" + }, "air_quality_standard": { "name": "Norma kvality vzduchu" }, diff --git a/custom_components/localthings/translations/de.json b/custom_components/localthings/translations/de.json index c34423f..cf31314 100644 --- a/custom_components/localthings/translations/de.json +++ b/custom_components/localthings/translations/de.json @@ -761,6 +761,9 @@ "air_filter_usage_hours": { "name": "Filternutzungsstunden" }, + "deodor_filter_usage": { + "name": "Filternutzung" + }, "air_quality_standard": { "name": "Luftqualitätsstandard" }, diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index bf47645..5b684be 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -761,6 +761,9 @@ "air_filter_usage_hours": { "name": "Filter usage hours" }, + "deodor_filter_usage": { + "name": "Filter usage" + }, "air_quality_standard": { "name": "Air quality standard" }, diff --git a/custom_components/localthings/translations/es.json b/custom_components/localthings/translations/es.json index fea0e9a..f4b7d2a 100644 --- a/custom_components/localthings/translations/es.json +++ b/custom_components/localthings/translations/es.json @@ -907,6 +907,9 @@ "air_filter_usage_hours": { "name": "Horas de uso del filtro" }, + "deodor_filter_usage": { + "name": "Uso del filtro" + }, "air_quality_standard": { "name": "Estándar de calidad del aire" }, diff --git a/custom_components/localthings/translations/it.json b/custom_components/localthings/translations/it.json index a8100c6..f13c398 100644 --- a/custom_components/localthings/translations/it.json +++ b/custom_components/localthings/translations/it.json @@ -761,6 +761,9 @@ "air_filter_usage_hours": { "name": "Ore di utilizzo filtro" }, + "deodor_filter_usage": { + "name": "Utilizzo filtro" + }, "air_quality_standard": { "name": "Standard qualità aria" }, diff --git a/custom_components/localthings/translations/ko.json b/custom_components/localthings/translations/ko.json index 61f7696..a27a9ef 100644 --- a/custom_components/localthings/translations/ko.json +++ b/custom_components/localthings/translations/ko.json @@ -761,6 +761,9 @@ "air_filter_usage_hours": { "name": "필터 사용 시간" }, + "deodor_filter_usage": { + "name": "필터 사용량" + }, "air_quality_standard": { "name": "공기질 기준" }, diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index 5b66704..839a6a6 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -761,6 +761,9 @@ "air_filter_usage_hours": { "name": "Filterverbruik (uren)" }, + "deodor_filter_usage": { + "name": "Filterverbruik" + }, "air_quality_standard": { "name": "Luchtkwaliteitsnorm" }, diff --git a/tests/fixtures/golden/refrigerator_winecellar.json b/tests/fixtures/golden/refrigerator_winecellar.json index b8a770b..38702ae 100644 --- a/tests/fixtures/golden/refrigerator_winecellar.json +++ b/tests/fixtures/golden/refrigerator_winecellar.json @@ -1,7 +1,5 @@ { "state_keys": [ - "air_filter_status", - "air_filter_usage", "alarm_code", "auto_door_opener", "auto_door_sound_control", @@ -9,6 +7,8 @@ "auto_door_voice_control", "cabinet_light_dim", "cabinet_light_switch", + "deodor_filter_status", + "deodor_filter_usage", "door_winecellar_open", "energy_kwh", "energy_saved_kwh", diff --git a/tests/test_refrigerator_winecellar_capabilities.py b/tests/test_refrigerator_winecellar_capabilities.py index 6bd2cba..c6dabe4 100644 --- a/tests/test_refrigerator_winecellar_capabilities.py +++ b/tests/test_refrigerator_winecellar_capabilities.py @@ -48,15 +48,21 @@ def test_auto_door_sound_control_present_here_only(): assert state["auto_door_voice_control"] is False -def test_deodor_filter_reuses_air_filter_shape(): - """Same entities as fridge.AIR_FILTER (see its own already-confirmed - percentage-not-raw-count reasoning), bound at this variant's own href.""" - assert fridge.DEODOR_FILTER.entities is fridge.AIR_FILTER.entities +def test_deodor_filter_has_its_own_keys_not_air_filters(): + """Same shape/reasoning as fridge.AIR_FILTER (percentage-not-raw-count, + see its own comment) but its own 'deodor_'-prefixed keys, not a shared + tuple -- AIR_FILTER's own docstring picked 'air_' specifically to avoid + colliding with WATER_FILTER's filter_usage/filter_status, and reusing + AIR_FILTER.entities verbatim here would silently recreate that same + collision if a unit ever reports both hrefs.""" + assert fridge.DEODOR_FILTER.entities != fridge.AIR_FILTER.entities state = _state() - assert state["air_filter_status"] == "normal" + assert state["deodor_filter_status"] == "normal" # The device's own sentinel, relayed as-is -- not a real 0-100 reading # on this unit, and no second dump to say what else -1 could mean. - assert state["air_filter_usage"] == -1 + assert state["deodor_filter_usage"] == -1 + assert "air_filter_usage" not in state + assert "air_filter_status" not in state def test_winecellar_pantry_zone_mode_options_and_write():