"""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 adds CO2, which neither air_purifier nor range_hood report.""" 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'}