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():