Address Opus review findings on the device-support commits above

- Fix a real bug: airconditioner.SOUND_MODE had no exists_fn, so on
  boards (issue #319's FAC) that never report a live 'mode' value,
  entity.py's default field-presence gate silently kept the select from
  ever registering in HA -- while adapter.flatten() (what the golden/tests
  read) has no such gate, so the tests passed while documenting behavior
  the opposite of what shipped. Gate on supportedModes' presence instead.
- Add airconditioner.MDS_ABSENCE_CLEAN for the CAC-class board's
  /mds/absenceclean/vs/0 -- byte-identical shape to issue #319's
  /csi/absenceclean/vs/0, confirmed rather than guessed, closing one more
  of that board's documented coverage-gap hrefs.
- Add missing translation state labels (all 6 languages) for
  edge_lighting_mode/edge_lighting_color/indicator_light_mode's raw device
  codes, so they render as real words instead of a raw '3000K' -> '3000 K'
  fallback.
- Fix an orphaned comment above SOUND_MODE that actually described the
  unrelated DISPLAY reuse, and correct two inaccurate rationale comments:
  the sound/voice ignore reason claimed a distinction from SOUND_MODE that
  this same dump contradicts, and the /csi/* ignore block's 'same
  reasoning as air_purifier.COVERAGE' precedent only actually covers 1 of
  its 5 hrefs.
- Correct the false 'no board-token match' claim in the FAC test file and
  golden-regression docstring -- 'FAC' is a real _BOARD_TOKEN_TO_KEY entry
  (for_device_by_model alone already resolves this board); add a test
  that actually exercises that path, which nothing previously did despite
  the docstring's claim.
- Drop a tautological burner-slot test that only re-asserted what the
  golden regression test already covers via the same code path.
This commit is contained in:
Marc Billow committed 2026-08-07 14:50:24 +00:00
1 parent c203bd42c5
commit e2dcc75ed5
13 files changed
+250 -68

No files matched your search

+1
View File
@@ -1,5 +1,6 @@
{
"state_keys": [
"absence_clean",
"absence_power_saving_active",
"absence_power_saving_mode",
"air_filter_pm1_status",
@@ -1,21 +1,42 @@
"""Tests for the AILP_DA-AC-FAC-02011_0000 air conditioner (issue #319).
This board has no `_BOARD_TOKEN_TO_KEY` entry (routes via `/oic/d`'s
`oic.d.airconditioner` type only) and reports several resources the sibling
TP1X_DA-AC-CAC-01001 board (issue #191) left as a documented gap:
/display/vs/0 and the /settings/sound/* trio (now shared with
air_purifier.py's identical shapes), plus two genuinely new hrefs
(/csi/absenceclean/vs/0, /csi/energysaving/vs/0).
This board resolves to the airconditioner registry two independent ways --
modelNum's 'FAC' board token (for_device_by_model) and /oic/d's
oic.d.airconditioner type (for_device_by_oic_type) agree -- and reports
several resources the sibling TP1X_DA-AC-CAC-01001 board (issue #191) left
as a documented gap: /display/vs/0 and the /settings/sound/* trio (now
shared with air_purifier.py's identical shapes), plus two genuinely new
hrefs (/csi/absenceclean/vs/0, /csi/energysaving/vs/0).
"""
from typing import cast
from custom_components.localthings.coordinator import LocalThingsCoordinator
from custom_components.localthings.entity import _is_included
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.by_type import (
for_device_by_model,
for_device_by_oic_type,
resolve,
)
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
_DEVICE_TYPES = ("oic.wk.d", "oic.d.airconditioner")
class _FakeCoordinator:
"""Minimal stand-in for entity._is_included's coordinator dependency --
same shape as test_entity.py's own fake, kept local rather than shared
across test modules."""
def __init__(self, last_resources):
self.last_resources = last_resources
def canonical_resources(self, subdevice):
return self.last_resources
def _resources():
return _load_device("airconditioner_ailp_fac")
@@ -31,6 +52,19 @@ def test_oic_device_type_resolves_to_airconditioner_registry():
assert reg is not None and reg.name == "airconditioner"
def test_board_token_resolves_to_airconditioner_registry():
"""'FAC' is a real _BOARD_TOKEN_TO_KEY entry -- for_device_by_model
alone (no device_types) already resolves this board correctly, same as
for_device_by_oic_type above; resolve()'s device_types-agreement isn't
covering an otherwise-unreachable path."""
resources = _resources()
info = resources["/information/vs/0"]
reg = for_device_by_model(
info["x.com.samsung.da.modelNum"], info["x.com.samsung.da.description"]
)
assert reg is not None and reg.name == "airconditioner"
def test_no_unbound_hrefs():
resources = _resources()
reg = resolve(resources, device_types=_DEVICE_TYPES)
@@ -39,15 +73,18 @@ def test_no_unbound_hrefs():
assert unbound == []
def test_sound_mode_bound_with_live_supported_modes():
def test_sound_mode_registers_despite_no_live_mode_value():
"""This dump has no current `mode` value yet, only supportedModes
(mute/tone/voice) -- confirms the select is bound (present in state,
even if unknown) and its options come from the live field rather than
a static tuple (see laundry.SOUND_MODE's docstring for why that's
preferred whenever the resource carries one)."""
(mute/tone/voice). entity.py's default field-presence gate would
otherwise keep the select from ever being created in HA even though
adapter.flatten() (checked below) has no such gate and would look
bound regardless -- exercise the real _is_included gate, not just
flatten(), so a regression here can't hide behind that gap again."""
bound, resources = _bound()
desc = next(b.desc for b in bound if b.desc.key == "sound_mode")
assert desc.options_field == "supportedModes"
entity = next(b for b in bound if b.desc.key == "sound_mode")
assert entity.desc.options_field == "supportedModes"
coord = cast(LocalThingsCoordinator, _FakeCoordinator(resources))
assert _is_included(entity, coord) is True
state = flatten(bound, resources)
assert "sound_mode" in state
assert state["sound_mode"] is None
+21 -6
View File
@@ -6,11 +6,11 @@ token -- this board was the one exception (its oneUiVersion self-reports
"7.0 Air conditioner", but 'CAC' had never been added to the board-token
table), so it silently fell back to common caps and lost its climate entity.
This dump is NOT fully covered yet -- three hrefs remain unbound
(absence-clean, `/settings/sound/optimization/vs/0`, smart-sensing-cooling),
all genuinely new to this board generation. That's a real device-support
gap, left documented here rather than guessed at, per the 'don't guess'
rule -- fixing the routing regression was the scope of #191.
This dump is NOT fully covered yet -- two hrefs remain unbound
(`/settings/sound/optimization/vs/0`, smart-sensing-cooling), both
genuinely new to this board generation. That's a real device-support gap,
left documented here rather than guessed at, per the 'don't guess' rule --
fixing the routing regression was the scope of #191.
/settings/sound/mode/vs/0, /settings/sound/output/vs/0 and
/settings/sound/volume/vs/0 used to be on this list too, until issue #319
@@ -23,6 +23,11 @@ until issue #288 (six System A/C cassette units on this same board) gave
real dump evidence for both -- airconditioner.EDGE_LIGHTING and
LIGHT_STATEFUL now cover them.
/mds/absenceclean/vs/0 used to be on this list too -- its {mode,
supportedModes: [On, Off]} shape is byte-identical to issue #319's
/csi/absenceclean/vs/0, confirmed on that sibling board rather than
guessed, so airconditioner.MDS_ABSENCE_CLEAN now covers it too.
/uvled/vs/0 and /filter/airdustPM1filter/vs/0 used to be on this list too,
until issue #270 (TP1X_FAC_TIME_23K) added real capabilities for both --
this board's own live filterUsage/filterStatus data on the PM1 filter binds
@@ -37,7 +42,6 @@ from tests.conftest import _load_device
_STILL_UNBOUND = frozenset(
{
"/mds/absenceclean/vs/0",
"/settings/sound/optimization/vs/0",
"/smartsensingcooling/vs/0",
}
@@ -70,6 +74,17 @@ def test_documented_coverage_gap_is_exactly_this_set():
assert set(unbound) == _STILL_UNBOUND
def test_mds_absenceclean_shares_csi_absenceclean_key():
"""/mds/absenceclean/vs/0's mode=='Off' on this dump -- confirms
MDS_ABSENCE_CLEAN actually binds (not just that the href stops
reporting as unbound)."""
resources = _resources()
reg = _reg(resources)
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
state = flatten(bound, resources)
assert state["absence_clean"] is False
def test_non_legacy_board_uses_the_generic_energy_scale():
"""This board reports /wind/strength/vs/0 (not /airflow/vs/0), so
is_legacy_board() is False and it must use the plain wh_to_kwh scale,
@@ -45,11 +45,12 @@ def test_alarm_code_and_child_lock_bound():
assert state["child_lock"] is True
def test_advertises_six_burner_slots_only_three_of_which_are_real():
"""Not a bug to fix -- the board's own /mode/vs/0 options genuinely
list six OperationState slots (issue #314); the reporter's physical
cooktop only has three. Locks in the current, documented behavior."""
bound, resources = _bound()
state = flatten(bound, resources)
for i in range(6):
assert f"burner_{i}_state" in state
# The reporter's original complaint -- six advertised burner slots when
# only three are physically present -- isn't tested here beyond what the
# golden regression test already locks in (burner_0_state..burner_5_state
# all present). It's expected, not a bug: the registry declares a generous
# static superset of slots per cooktop.py's own comment, and this board's
# /mode/vs/0 rep carries no field distinguishing a real slot from an
# advertised-but-nonexistent one for a test to assert against -- the user
# disabling the three extra entities is the correct fix, not something the
# integration can filter automatically.
+8 -7
View File
@@ -135,9 +135,11 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner():
def test_registry_reproduces_golden_state_keys_for_airconditioner_ailp_fac():
"""AILP_DA-AC-FAC-02011_0000 (issue #319) has no board-token match --
resolves purely via /oic/d's oic.d.airconditioner type, exercising the
device_types-only path through resolve()."""
"""AILP_DA-AC-FAC-02011_0000 (issue #319) resolves both ways --
modelNum's 'FAC' board token and /oic/d's oic.d.airconditioner type
independently agree on the airconditioner registry. Passes
device_types through resolve() to exercise that agreement, not because
the board token is absent."""
from tests.conftest import _load_device
resources = _load_device("airconditioner_ailp_fac")
@@ -1173,10 +1175,9 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner_cac():
0.16.0 when oneUiVersion detection was dropped, since 'CAC' had never
been added to the modelNum board-token table. Resolved via the new 'CAC'
token onto the existing airconditioner registry. Not fully covered yet --
three hrefs remain unbound (absence-clean, sound-optimization,
smart-sensing-cooling), all genuinely new to this board generation and
out of scope for the routing fix; see test_airconditioner_cac.py for
the documented gap."""
two hrefs remain unbound (sound-optimization, smart-sensing-cooling),
both genuinely new to this board generation and out of scope for the
routing fix; see test_airconditioner_cac.py for the documented gap."""
from tests.conftest import _load_device
resources = _load_device("airconditioner_cac")