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.
114 lines
4.6 KiB
Python
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
|