Files
localthings/tests/test_airconditioner_ventilation_windfree.py
Marc Billow a7dc1db8ff Extract usable parts of PR #316 (System Fresh Air Ventilator support)
PR #316 (fork stale by several months, most of its ~2200-line diff was drift
against main rather than real changes) proposed device support for the
Samsung System Fresh Air Ventilator (ACA-KR-TP2-21-AN9000). Extracted what
holds up, adapted to this project's conventions, and left out what doesn't:

Extracted:
- ventilation_mode select on CLIMATE's own href, gated via
  _is_ventilation_mode_device so it can only ever bind on a device whose
  entire supportedModes set is Purification/Ventilation/SmartVentilation --
  verified against every real AC fixture in the corpus to confirm it can't
  false-positive on an actual air conditioner's climate card.
- WINDFREE / WINDSLEEP switches on their own dedicated hrefs.
- A CO2 sensor on AIR_QUALITY, matching air_monitor.SENSORS' already-bound
  device_class='carbon_dioxide'/unit='ppm' descriptor for the same field
  shape rather than guessing fresh.
- HEPA_FILTER / DEVICE_ACTIVE reuse from air_purifier.py.
- Removing /airlevelcheck/vs/0 from _AC_IGNORED and binding
  air_purifier.AIR_LEVEL_CHECK in its place: the PR's claim that this
  project's old "scheduler plumbing" description was wrong turned out to
  be independently verifiable against two of our own existing fixtures
  (airconditioner_cac and airconditioner_tp1x_da_ac_rac_01011 both already
  carry real, populated periodicSensingActivationState/autoExeState
  values), so this benefits existing users, not just the one new device.

Left out:
- Unit/device_class ('ug/m3', pm10/pm25/pm1) on the existing dust/
  fine_dust/super_fine_dust sensors, sourced from an unverified third-party
  screenshot description. air_monitor.py already has an explicit, reasoned
  rejection of this exact mapping for the exact same three fields:
  Samsung's PM10/PM2.5 convention doesn't confirm where a third tier or a
  PM1 reading fits, and a wrong guess mislabels the reading forever.
- A standalone common.POWER switch -- contradicts this registry's own
  documented design (power is deliberately the climate entity's job) and
  would affect every AC user, not just this device.
- Promoting wind/swing to independent selects for every AC user -- a UX
  opinion, not a coverage necessity, and out of scope for this device's
  own support.
- A model-name diagnostic sensor -- /information/vs/0 is already covered
  via the global ignore list, so this wasn't closing an actual gap.

No raw diagnostics dump for this model was ever attached to PR #316, so
there's no fixture for it here (fabricating one would violate this
project's fixture-integrity rule) -- see
tests/test_airconditioner_ventilation_windfree.py's module docstring.
2026-08-07 16:38:09 +00:00

114 lines
4.6 KiB
Python

"""Tests for the ventilation-mode/Wind-Free/Wind-Sleep additions extracted
from PR #316 (Samsung "System Fresh Air Ventilator", model
ACA-KR-TP2-21-AN9000, vid DA-AC-DIFFUSER-01001).
No raw diagnostics dump for this model was available -- PR #316 never
attached one, only Korean code comments describing field shapes the
contributor said they observed. Per this project's fixture-integrity rule
(a fixture must record what hardware actually did, not a third party's
prose about it), there's no `airconditioner_*_device.json` fixture for
this model here. These tests instead exercise the gating logic directly
against hand-built reps matching those quoted shapes, clearly distinct
from this suite's fixture-backed tests, and check the new gate doesn't
false-positive against every real AC fixture already in the corpus.
If a real diagnostics dump for this model ever surfaces (tracked as a
follow-up device-support issue), replace this file with a proper
fixture + golden + capability test per the usual workflow, and drop the
disclaimer above.
"""
import glob
import json
import os
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import for_device_by_model
from custom_components.localthings.registry.capabilities.airconditioner import (
WINDFREE,
WINDSLEEP,
_is_ventilation_mode_device,
)
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
_FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures")
def _all_airconditioner_fixture_names():
names = []
for path in glob.glob(os.path.join(_FIXTURES_DIR, "airconditioner*_device.json")):
name = os.path.basename(path)[: -len("_device.json")]
with open(path) as f:
info = json.load(f)
rep = next(
(e["rep"] for e in info.get("device0", []) if e.get("href") == "/information/vs/0"),
None,
)
if rep is not None: # for_device_by_model needs /information/vs/0
names.append(name)
return names
def test_ventilation_mode_gate_never_false_positives_on_real_ac_fixtures():
"""None of the real air-conditioner fixtures in this corpus use the
Purification/Ventilation/SmartVentilation vocabulary -- confirms
_is_ventilation_mode_device can't turn a real AC's climate card into
this select."""
for name in _all_airconditioner_fixture_names():
resources = _load_device(name)
reg = for_device_by_model(
resources["/information/vs/0"]["x.com.samsung.da.modelNum"],
resources["/information/vs/0"]["x.com.samsung.da.description"],
)
if reg is None or reg.name != "airconditioner":
continue
mode_rep = resources.get("/mode/vs/0")
if not mode_rep:
continue
assert _is_ventilation_mode_device(mode_rep, resources) is False, name
def test_ventilation_mode_gate_matches_diffuser_shape():
"""PR #316: supportedModes exactly {Purification, Ventilation,
SmartVentilation} -- the vocabulary that makes climate.py's hvac_mode
collapse to one stuck value with no way to tell the three apart."""
rep = {
"x.com.samsung.da.modes": ["Purification"],
"x.com.samsung.da.supportedModes": ["Purification", "Ventilation", "SmartVentilation"],
}
assert _is_ventilation_mode_device(rep, {}) is True
def test_ventilation_mode_gate_rejects_partial_overlap():
"""A real AC reporting an unrelated mode alongside one of these three
words (coincidence, not this device) must not gate in -- the check is
'subset of', not 'intersects'."""
rep = {"x.com.samsung.da.supportedModes": ["Ventilation", "Cool", "Heat"]}
assert _is_ventilation_mode_device(rep, {}) is False
def _bind(capability, href, rep):
resources = {href: rep}
bound = discover(resources, {href: [capability]}, [])
return flatten(bound, resources)
def test_windfree_and_windsleep_read_their_own_hrefs():
"""PR #316's quoted rep shape: {'x.com.samsung.da.windfree': 'On'/'Off',
'x.com.samsung.da.displaycondition': 'normal'} (displaycondition is a
read-only UI-visibility flag, deliberately not modeled)."""
windfree_state = _bind(
WINDFREE,
"/modeoption/windfree/vs/0",
{"x.com.samsung.da.windfree": "On", "x.com.samsung.da.displaycondition": "normal"},
)
assert windfree_state["windfree"] is True
windsleep_state = _bind(
WINDSLEEP,
"/modeoption/windsleep/vs/0",
{"x.com.samsung.da.windsleep": "Off", "x.com.samsung.da.displaycondition": "normal"},
)
assert windsleep_state["windsleep"] is False