air_purifier: share has_sensor_type and disable co2 by default
Hoist airconditioner._has_sensor_type to common.has_sensor_type next to sensor_item_value, add the issue #127 stub-rep carve-out, and match the AC family's enabled_default=False on the purifier CO2 entity.
This commit is contained in:
1 parent
296ba18cba
commit
171316bd95
7 files changed
+82
-41
No files matched your search
@@ -34,7 +34,13 @@ from ..entities import (
|
||||
SwitchDesc,
|
||||
TimeDesc,
|
||||
)
|
||||
from .common import epoch_to_utc, filter_usage_percent, int_or_none, sensor_item_value
|
||||
from .common import (
|
||||
epoch_to_utc,
|
||||
filter_usage_percent,
|
||||
has_sensor_type,
|
||||
int_or_none,
|
||||
sensor_item_value,
|
||||
)
|
||||
from .laundry import bool_option_exists, bool_option_value, option_value, option_write
|
||||
|
||||
# Newer TP1X_DA-AC-AIR-class boards (issue #130) report fan modes directly
|
||||
@@ -51,18 +57,6 @@ def _has_top_level_modes(rep, resources):
|
||||
return isinstance(rep.get("x.com.samsung.da.supportedModes"), (list, tuple))
|
||||
|
||||
|
||||
def _has_sensor_type(type_):
|
||||
"""True when /sensors/vs/0's items[] lists an item of this type."""
|
||||
|
||||
def fn(rep, resources):
|
||||
return any(
|
||||
isinstance(i, dict) and i.get("x.com.samsung.da.type") == type_
|
||||
for i in (rep.get("x.com.samsung.da.items") or [])
|
||||
)
|
||||
|
||||
return fn
|
||||
|
||||
|
||||
# Columns: key, icon, device item type, state_class, device_class, unit.
|
||||
# state_class is what makes Home Assistant keep long-term statistics --
|
||||
# without one, a reading is only in the short-term recorder history and
|
||||
@@ -134,7 +128,8 @@ AIR_QUALITY = Capability(
|
||||
# CO2 (issue #387) -- same field/shape air_monitor.SENSORS already
|
||||
# models with device_class='carbon_dioxide'/unit='ppm'. Gated on the
|
||||
# type being listed so boards that don't report it (every current
|
||||
# fixture) don't grow an empty entity.
|
||||
# fixture) don't grow an empty entity. Disabled by default for the
|
||||
# same reason as airconditioner.AIR_QUALITY (issue #166).
|
||||
SensorDesc(
|
||||
key="co2",
|
||||
field="x.com.samsung.da.items",
|
||||
@@ -142,7 +137,8 @@ AIR_QUALITY = Capability(
|
||||
device_class="carbon_dioxide",
|
||||
state_class="measurement",
|
||||
unit="ppm",
|
||||
exists_fn=_has_sensor_type("CO2"),
|
||||
exists_fn=has_sensor_type("CO2"),
|
||||
enabled_default=False,
|
||||
value_fn=lambda items: sensor_item_value(items, "CO2"),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -23,7 +23,7 @@ from ..entities import (
|
||||
SwitchDesc,
|
||||
)
|
||||
from . import common
|
||||
from .common import filter_usage_hours, filter_usage_percent, normalize_temp_unit
|
||||
from .common import filter_usage_hours, filter_usage_percent, has_sensor_type, normalize_temp_unit
|
||||
from .laundry import option_write
|
||||
|
||||
|
||||
@@ -119,26 +119,6 @@ def _sensor_item_value(items, type_):
|
||||
return None
|
||||
|
||||
|
||||
def _has_sensor_type(type_):
|
||||
"""True when /sensors/vs/0's items[] lists an item of this type.
|
||||
|
||||
This only proves the type is *listed*, not that the reading is real:
|
||||
issue #166 (ARTIK051_PRAC_20K) lists all five types with permanent-zero
|
||||
values on units the reporter confirmed don't have the hardware. So
|
||||
entities gated on this stay disabled by default (see AIR_QUALITY) rather
|
||||
than existence-gated further, to avoid silently dropping real readings
|
||||
on hardware not yet seen.
|
||||
"""
|
||||
|
||||
def fn(rep, resources):
|
||||
return any(
|
||||
isinstance(i, dict) and i.get("x.com.samsung.da.type") == type_
|
||||
for i in (rep.get("x.com.samsung.da.items") or [])
|
||||
)
|
||||
|
||||
return fn
|
||||
|
||||
|
||||
# Canonical AC resource hrefs. climate.py binds HREF_MODE and reads the
|
||||
# CLIMATE_CONSUMED_HREFS siblings off the coordinator snapshot; declared once
|
||||
# here so climate.py and the coverage list below can't drift out of sync.
|
||||
@@ -1570,7 +1550,7 @@ WINDSLEEP = Capability(
|
||||
# /sensors/vs/0 items[] carry live air-quality readings. CleanLevel is
|
||||
# corroborated as numeric by a top-level x.com.samsung.da.cleanLevel scalar,
|
||||
# so it's a measurement; the others stay string diagnostics (see
|
||||
# _sensor_item_value). All disabled by default: _has_sensor_type only proves
|
||||
# _sensor_item_value). All disabled by default: has_sensor_type only proves
|
||||
# the item type is listed, not that the sensor is real (see its docstring).
|
||||
AIR_QUALITY = Capability(
|
||||
href="/sensors/vs/0",
|
||||
@@ -1582,7 +1562,7 @@ AIR_QUALITY = Capability(
|
||||
icon="mdi:broom",
|
||||
entity_category="diagnostic",
|
||||
state_class="measurement",
|
||||
exists_fn=_has_sensor_type("CleanLevel"),
|
||||
exists_fn=has_sensor_type("CleanLevel"),
|
||||
enabled_default=False,
|
||||
value_fn=lambda items: _int(_sensor_item_value(items, "CleanLevel")),
|
||||
),
|
||||
@@ -1592,7 +1572,7 @@ AIR_QUALITY = Capability(
|
||||
field="x.com.samsung.da.items",
|
||||
icon=icon,
|
||||
entity_category="diagnostic",
|
||||
exists_fn=_has_sensor_type(type_),
|
||||
exists_fn=has_sensor_type(type_),
|
||||
enabled_default=False,
|
||||
value_fn=lambda items, t=type_: _sensor_item_value(items, t),
|
||||
)
|
||||
@@ -1618,7 +1598,7 @@ AIR_QUALITY = Capability(
|
||||
device_class="carbon_dioxide",
|
||||
state_class="measurement",
|
||||
unit="ppm",
|
||||
exists_fn=_has_sensor_type("CO2"),
|
||||
exists_fn=has_sensor_type("CO2"),
|
||||
enabled_default=False,
|
||||
value_fn=lambda items: _int(_sensor_item_value(items, "CO2")),
|
||||
),
|
||||
|
||||
@@ -299,6 +299,30 @@ def sensor_item_value(items, sensor_type, index=0):
|
||||
return None
|
||||
|
||||
|
||||
def has_sensor_type(type_):
|
||||
"""True when /sensors/vs/0's items[] lists an item of this type.
|
||||
|
||||
This only proves the type is *listed*, not that the reading is real:
|
||||
issue #166 (ARTIK051_PRAC_20K) lists all five types with permanent-zero
|
||||
values on units the reporter confirmed don't have the hardware. So
|
||||
entities gated on this stay disabled by default rather than
|
||||
existence-gated further, to avoid silently dropping real readings on
|
||||
hardware not yet seen.
|
||||
|
||||
is_stub_rep(rep) keeps the stub carve-out (see entity._is_included /
|
||||
ENERGY_METER, issue #127): an explicit exists_fn otherwise bypasses it
|
||||
and would drop the entity when /device/0 returns a not-yet-fetched stub.
|
||||
"""
|
||||
|
||||
def fn(rep, resources):
|
||||
return is_stub_rep(rep) or any(
|
||||
isinstance(i, dict) and i.get("x.com.samsung.da.type") == type_
|
||||
for i in (rep.get("x.com.samsung.da.items") or [])
|
||||
)
|
||||
|
||||
return fn
|
||||
|
||||
|
||||
# OCF-native / vendor '-vs' fallback pairs for power, kids-lock, remote
|
||||
# control: each exists as both a standard OCF resource (/power/0,
|
||||
# oic.r.switch.binary, plain boolean 'value') and a Samsung vendor
|
||||
|
||||
@@ -110,6 +110,7 @@ def test_co2_matches_air_monitor_mapping():
|
||||
assert desc.unit == "ppm"
|
||||
assert desc.state_class == "measurement"
|
||||
assert desc.exists_fn is not None
|
||||
assert desc.enabled_default is False
|
||||
assert (
|
||||
desc.value_fn([{"x.com.samsung.da.type": "CO2", "x.com.samsung.da.value": ["498"]}]) == 498
|
||||
)
|
||||
|
||||
@@ -91,10 +91,14 @@ def test_co2_reads_when_the_items_list_includes_the_type():
|
||||
|
||||
desc = next(e for e in air_purifier.AIR_QUALITY.entities if e.key == "co2")
|
||||
assert desc.exists_fn is not None
|
||||
assert desc.enabled_default is False
|
||||
assert desc.exists_fn({"x.com.samsung.da.items": []}, {}) is False
|
||||
assert (
|
||||
desc.exists_fn({"x.com.samsung.da.items": [{"x.com.samsung.da.type": "CO2"}]}, {}) is True
|
||||
)
|
||||
# /device/0 stub must not drop co2 while its field-gated siblings still
|
||||
# register -- platforms enumerate bound once (issue #127).
|
||||
assert desc.exists_fn({"href": "/sensors/vs/0"}, {}) is True
|
||||
|
||||
|
||||
def test_filter_progress_reads_named_consumable_item():
|
||||
|
||||
@@ -986,7 +986,7 @@ def test_air_quality_disabled_by_default():
|
||||
SuperFineDust readings with no such scalar, so requiring it would
|
||||
silently drop real readings on hardware this repo hasn't seen yet on an
|
||||
AC. These stay bound whenever the item type is listed (see
|
||||
_has_sensor_type) and disabled by default instead, same precedent as
|
||||
has_sensor_type) and disabled by default instead, same precedent as
|
||||
fridge.rack_count / cooktop.paired_hood_model / tropical_night_mode --
|
||||
units that do have the sensor can enable it themselves."""
|
||||
for key in ("clean_level", "odor", "dust", "fine_dust", "super_fine_dust"):
|
||||
@@ -994,6 +994,17 @@ def test_air_quality_disabled_by_default():
|
||||
assert desc.enabled_default is False, key
|
||||
|
||||
|
||||
def test_air_quality_included_on_stub_rep():
|
||||
"""exists_fn would otherwise drop every gated sensor when /device/0
|
||||
returns a not-yet-fetched stub, while field-gated siblings still
|
||||
register (issue #127)."""
|
||||
stub = {"href": "/sensors/vs/0"}
|
||||
for key in ("clean_level", "odor", "dust", "fine_dust", "super_fine_dust", "co2"):
|
||||
desc = next(e for e in airconditioner.AIR_QUALITY.entities if e.key == key)
|
||||
assert desc.exists_fn is not None
|
||||
assert desc.exists_fn(stub, {}) is True, key
|
||||
|
||||
|
||||
def test_air_quality_absent_when_no_sensor_items():
|
||||
"""A board whose /sensors/vs/0 carries an empty items[] (the cool-only
|
||||
RAC variant) binds no air-quality entities -- exists_fn gates each on its
|
||||
|
||||
@@ -417,6 +417,31 @@ class TestEnergyMeter:
|
||||
assert pw.exists_fn({"x.com.samsung.da.cumulativePower": "5"}, {}) is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# has_sensor_type. Presence gate for /sensors/vs/0 items[] types, shared by
|
||||
# airconditioner.AIR_QUALITY and air_purifier.AIR_QUALITY. The stub carve-out
|
||||
# is the same contract ENERGY_METER documents for issue #127.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestHasSensorType:
|
||||
def test_true_when_type_is_listed(self):
|
||||
fn = common.has_sensor_type("CO2")
|
||||
assert fn({"x.com.samsung.da.items": [{"x.com.samsung.da.type": "CO2"}]}, {}) is True
|
||||
|
||||
def test_false_when_type_is_absent(self):
|
||||
fn = common.has_sensor_type("CO2")
|
||||
assert fn({"x.com.samsung.da.items": [{"x.com.samsung.da.type": "Dust"}]}, {}) is False
|
||||
assert fn({"x.com.samsung.da.items": []}, {}) is False
|
||||
assert fn({}, {}) is False
|
||||
|
||||
def test_true_on_stub_rep(self):
|
||||
"""A true stub -- /device/0's {"href": "..."} "not fetched yet"
|
||||
marker -- must keep the entity so sub-polls can populate it."""
|
||||
fn = common.has_sensor_type("CO2")
|
||||
assert fn({"href": "/sensors/vs/0"}, {}) is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AI energy-saving level. '0' is off; supportedAiLevel lists the additional
|
||||
# level(s) on offer. A single-entry list (issue #21 fridge, issue #40 washer)
|
||||
|
||||
Reference in new issue
Block a user