Files
localthings/tests/test_air_monitor_capabilities.py
JayChickenK 33a06b9ae8 air_purifier: expose CO2 when the device lists it
Issue #387's TP1X_DA-AC-AIR-class board reports a CO2 item on
/sensors/vs/0. Same field/shape air_monitor.SENSORS already models
(carbon_dioxide / ppm). Gated with exists_fn so boards that don't
list the type (every current fixture) don't grow an empty entity.
2026-08-18 11:04:25 +02:00

116 lines
4.0 KiB
Python

"""Tests for the Samsung Air Monitor Plus family (ASM-KR-TP1-22-ACMB1M,
issue #210) -- a standalone, battery-powered air-quality sensor puck. No
controllable appliance state at all beyond the do-not-disturb window.
"""
from datetime import time as dt_time
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import for_device_by_oic_type, resolve
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
def _air_monitor():
resources = _load_device("air_monitor")
reg = resolve(resources)
return reg, resources
def _bound():
reg, resources = _air_monitor()
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
def _state():
bound, resources = _bound()
return flatten(bound, resources)
def _desc(key):
bound, _ = _bound()
return next(b.desc for b in bound if b.desc.key == key)
def test_resolves_to_air_monitor_registry():
reg, _ = _air_monitor()
assert reg is not None and reg.name == "air_monitor"
def test_resolves_via_oic_type_too():
"""This board's real dump carries /oic/d's device type
('x.com.st.d.airqualitysensor') as well as the 'ASM' modelNum token --
both paths must agree."""
reg = for_device_by_oic_type(("x.com.st.d.airqualitysensor",))
assert reg is not None and reg.name == "air_monitor"
def test_no_unbound_hrefs():
reg, resources = _air_monitor()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_air_quality_sensors_read_from_shared_items_decode():
"""Same /sensors/vs/0 {type, value} shape and common.sensor_item_value
decode air_purifier.AIR_QUALITY already uses -- this board lists CO2
unconditionally, unlike the exists_fn-gated purifier descriptor."""
state = _state()
assert state["dust"] == 31
assert state["fine_dust"] == 23
assert state["super_fine_dust"] == 18
assert state["odor"] == 1
assert state["clean_level"] == 2
assert state["co2"] == 498
def test_second_value_slot_is_not_exposed():
"""Dust/fine_dust/super_fine_dust each carry a second `value[1]` grade
code (e.g. Dust's ['31', '2']) with no supported-values field anywhere
in the dump to explain its scale -- per the 'still never invent... from
nothing' rule, only the raw reading (value[0]) is bound, not a second
'dust_level' or similar entity for that code."""
state = _state()
assert "dust_level" not in state
assert "dust_grade" not in state
def test_humidity_and_battery_present():
state = _state()
assert state["humidity"] == 57
assert state["battery"] == 80
assert state["battery_charging"] is False
def test_air_quality_standard_present():
state = _state()
assert state["air_quality_standard"] == "AirCleanAssociation"
def test_dnd_read_contract():
state = _state()
assert state["dnd"] is False
assert state["dnd_start"] == dt_time(14, 0)
assert state["dnd_end"] == dt_time(22, 0)
def test_dnd_write_contracts_are_flagged_educated_guesses():
"""Issue #210: no idle-vs-active dump pair exists for /dnd/vs/0 (DND
was never toggled in the one dump this board has), so these write
contracts are educated guesses -- symmetric with the read side's own
string/time-format shape, not invented from nothing, but still
unconfirmed on real hardware. Only the wiring is tested here; whether
the device actually accepts these writes needs a reporter to confirm."""
dnd = _desc("dnd")
path, body = dnd.write_fn("On", {})
assert path == ["dnd", "vs", "0"]
assert body == {"x.com.samsung.da.value": "true"}
path, body = dnd.write_fn("Off", {})
assert body == {"x.com.samsung.da.value": "false"}
start = _desc("dnd_start")
path, body = start.write_fn(dt_time(9, 30), {})
assert path == ["dnd", "vs", "0"]
assert body == {"x.com.samsung.da.startTime": "09:30:00"}