Stop double-converting filterUsage on AIR_FILTER/HEPA_FILTER (#330)

filterUsage is already a 0-100 percentage on every confirmed family,
including ARTIK051_PRAC: filterStatus flips to 'wash' at
filterUsage == '100' regardless of filterCapacity (60/224/500 across
other fixtures), which only holds if filterUsage is already a percent.
filter_usage_percent() divided by filterCapacity again, reading a
filter due for washing as 20% fresh.

air_filter_usage_hours had the mirror problem: it read filterUsage
directly as an hour count with device_class=duration, when the field
is a percent. It's now derived from the percentage and filterCapacity
(new filter_usage_hours() in common.py) instead.
This commit is contained in:
Marc Billow committed 2026-08-09 00:37:09 +00:00
1 parent efea9e9888
commit 07c20e82aa
4 files changed
+54 -27

No files matched your search

@@ -22,7 +22,7 @@ from ..entities import (
SwitchDesc,
)
from . import common
from .common import filter_usage_percent, normalize_temp_unit
from .common import filter_usage_hours, filter_usage_percent, normalize_temp_unit
from .laundry import option_write
@@ -854,16 +854,17 @@ AIR_FILTER = Capability(
icon="mdi:air-filter",
entity_category="diagnostic",
),
# Lifetime hour counter, resets only on filter replacement.
# Lifetime hour counter, resets only on filter replacement. Derived
# from the percentage and filterCapacity (issue #330) -- filterUsage
# itself is not an hour count.
SensorDesc(
key="air_filter_usage_hours",
field="x.com.samsung.da.filterUsage",
rep_fn=filter_usage_hours,
device_class="duration",
state_class="total_increasing",
unit_fn=_filter_unit,
icon="mdi:air-filter",
entity_category="diagnostic",
value_fn=_int,
),
# Locally writable alarm threshold (see _threshold_write); only
# surfaces where supportedFilterDesiredUsage is advertised.
@@ -925,13 +926,12 @@ AIR_FILTER_PM1 = Capability(
),
SensorDesc(
key="air_filter_pm1_usage_hours",
field="x.com.samsung.da.filterUsage",
rep_fn=filter_usage_hours,
device_class="duration",
state_class="total_increasing",
unit_fn=_filter_unit,
icon="mdi:air-filter",
entity_category="diagnostic",
value_fn=_int,
exists_fn=_has_filter_field("x.com.samsung.da.filterUsage"),
),
SelectDesc(
@@ -72,15 +72,25 @@ def epoch_to_utc(value):
def filter_usage_percent(rep):
"""Filter usage as a percentage of rated capacity. Several families
report `filterUsage` as a raw count in `filterCapacityUnit` (e.g. 100 of
a 500-hour capacity), so a plain value with a '%' unit would be wrong.
Returns None when capacity is missing/zero."""
used = _num(rep.get("x.com.samsung.da.filterUsage"))
"""Filter usage as a percentage. `filterUsage` is already 0-100 on every
family confirmed so far, including ARTIK051_PRAC (issue #330): its own
fixture and three live heads all show `filterStatus == 'wash'` at
`filterUsage == '100'` regardless of `filterCapacity` (60/224/500 across
other families' fixtures), which only holds if `filterUsage` is already
a percent -- dividing by capacity again would read that filter as fresh
at 20%."""
return int_or_none(rep.get("x.com.samsung.da.filterUsage"))
def filter_usage_hours(rep):
"""Elapsed filter hours, derived from the percentage and capacity rather
than read off `filterUsage` directly -- `filterUsage` is a percent, not
an hour count (issue #330). Returns None when capacity is missing/zero."""
pct = filter_usage_percent(rep)
cap = _num(rep.get("x.com.samsung.da.filterCapacity"))
if used is None or not cap:
if pct is None or not cap:
return None
return round(used / cap * 100)
return round(pct / 100 * cap, 1)
def normalize_temp_unit(raw, default="°F"):