Commit Graph
6 Commits
Author SHA1 Message Date
Marc Billow 5a73a25005 Address independent code review findings on PR #167
Two real bugs, both latent (no shipped fixture exercised them), plus a
consistency gap and a couple of correctness/DRY nits flagged by review:

- async_set_fan_mode resolved a fan_mode label against the static
  _FAN_TO_DEVICE reverse map before checking whether the resulting code is
  actually one of the unit's own supportedModes. A board using non-standard
  wind-strength codes while still spelling a standard-looking label in
  modesName (e.g. codes "31"-"33" named "Low"/"High"/"Turbo") would silently
  write a code ("1"/"3"/"4") the device never advertised. Now validates the
  static hit against the unit's own supported codes before trusting it,
  falling through to the live modesName scan otherwise.

- air_purifier.WIND_STRENGTH_FAN reused key='fan', the same key as FAN in
  the same registry -- BoundEntity's unique_id is built from key alone, not
  href, so a board reporting both hrefs would have one fan entity silently
  shadow the other. Renamed to 'wind_strength_fan' (translation_key
  unchanged). No shipped fixture reports both hrefs today, but the two caps
  living in the same registry made this a real latent hazard, the exact one
  AIRFLOW_GENERIC's own comment already documents and deliberately avoids.

- microwave.py's cooking_mode select still used a static, union-of-all-
  dumps mode list, even though both shipped microwave fixtures already
  report x.com.samsung.da.supportedModes on /mode/vs/0 -- the same shape
  oven._oven_mode_options was just built to prefer over exactly this kind
  of static list (issue #138's follow-up, this same PR's skill update).
  ME7500D advertises 4 modes; the select was offering 11. Applied the same
  live-first, static-fallback pattern.

- Added the issue #152 fixture the microwave lamp fix was missing (the
  SKILL.md step this PR itself added asks for one).

- climate.py's _legacy_airflow rebuilt a 2-key presence dict from
  coordinator.resource()'s truthiness, which collapses "href absent" and
  "href present with an empty {} rep" to the same falsy value -- while
  is_legacy_board (and discover()'s own binding) test key membership, not
  truthiness. Simplified to pass last_resources through directly, matching
  is_legacy_board's actual contract instead of a cheaper approximation of
  it, so the "can never disagree" claim in both docstrings is actually true.

- Hoisted the 'power' payload branch duplicated verbatim across
  _airflow_fan_write/_fan_write/_wind_strength_fan_write into one
  _power_write helper (registry/capabilities/air_purifier.py).

- Removed two now-unused imports (test_air_dresser_capabilities.py,
  test_air_purifier_vtww_fan.py) and replaced a tautological
  code-in-_DEVICE_TO_FAN check with one that actually exercises the live
  climate entity's fan_modes/fan_mode (test_climate_ac_modes.py).

- Fixed a pre-existing (not from this PR) no-op test on main --
  test_registry_reproduces_golden_state_keys_for_induction_cooktop computed
  golden/state_keys and never asserted on them.

756 tests pass.
2026-07-28 14:37:32 +00:00
Marc Billow daa7546208 Add device support for Wind-Free 2-in-1 AC TP2X_FAC_BORA_21K (#150, #153)
This board (a floor-standing + wall-mounted indoor unit pair sharing one
outdoor unit and one local IP) reports no oneUiVersion and carries the
'_FAC_' modelNum token, which no existing routing rule matched -- it fell
back to 'unknown' and exposed nothing but a power switch, with no climate
entity generated at all (both issues' reported symptom).

Once routed to the existing airconditioner registry, it binds cleanly
against the exact same CLIMATE composite every other room-AC family uses --
same Cool/Dry/Wind/AIComfort mode vocabulary, same wind-strength/humidity/
filter/energy resource shapes already modeled. Only two hrefs are unique to
this board: /subdevices/vs/0 (an opaque paired-subdevice id list -- issue
#150 asked whether the second indoor unit can be controlled separately;
it can't through this or any other resource in the dump, the same
"remote device ids, not locally actionable" role as the existing
/remotedeviceinfo/vs/0 ignore) and /runn/vs/0 (a single undocumented int
with no supported-values list to interpret). Both added to _AC_IGNORED
rather than guessed at.
2026-07-28 14:09:21 +00:00
Marc Billow 5547c401ed Address independent review findings on the RAC finish-up branch
- Pin the FAN_ONLY reverse-write fallback to 'Wind' (the original single
  spelling) instead of letting it silently flip to 'Fan' just because
  'Fan' was added second to the dict -- _device_code_for_hvac() resolves
  the code from a unit's own supportedModes first, so this dict is only
  a fallback for a unit reporting none at all, and that fallback
  shouldn't change behavior as an unintended side effect of insertion
  order.
- Add direct tests for _preset_to_ha() (pure function, previously
  untested) and the FAN_ONLY fallback pin.
- Cross-reference the AC family's inverted Light_On/Light_Off polarity
  against air_purifier.py's plain-polarity use of the same token name on
  the same resource name, so a future refactor doesn't assume they're
  the same thing.
- Fix a one-column continuation-line misalignment and a stale PR-number
  reference in a comment.
2026-07-27 05:11:58 +00:00
Marc Billow fa8f1b8675 Add cool-only global RAC support: WindFree, Auto mode, display light
Finishes PR #91's contribution (pedroperosin) with the requested review
changes applied, on our own branch:

- Preset resolution is now fully dynamic, read from each unit's own
  /mode/convenient/vs/0 supportedModes instead of a static per-model
  table -- any board's convenient modes (including WindFree's Nano/
  NanoSleep) surface without code changes. Unlabelled codes across
  existing fixtures (longwind, motionindirect, motiondirect, drycomfort)
  plus the two new WindFree ones are added to en.json/nl.json.
- 'Auto' now maps to HVACMode.AUTO instead of HEAT_COOL: these are
  single-setpoint "device decides" units, not two-setpoint heat+cool
  ones. 'Fan' is added alongside 'Wind' as a second FAN_ONLY spelling;
  a new _device_code_for_hvac() picks the code from the unit's own
  supportedModes since the flat map can't disambiguate two device codes
  mapping to one HA value.
- Detection gains a hyphenated '-RAC-' modelNum fallback (alongside the
  existing '_RAC_') for cool-only global RAC variants whose
  /otninformation/vs/0 ships no swVersionInfo block.
- Adds a display_light switch sourced from /mode/vs/0's opaque options
  blob (inverted Light_On/Light_Off token) for boards with no dedicated
  /light/vs/0 switch.

Write-path behavioural change (touches the path iterated on across
#9/#17/#27/#38/#54): power now targets the vendor /power/vs/0 instead of
the OCF /power/0, and _is_on() reads vendor-first. /power/0 is absent on
several known AC boards, so the previous OCF-first read/write pair could
report and act on stale state -- consistent with #53's "can turn on but
not off". Target temperature now picks its channel (OCF pair vs. vendor
items[]) based on which the unit actually reports, reading and writing
the same one.

The vendor temperature write and the new display-light write both carry
only the changed field(s), not the whole resource -- confirmed sufficient
on the wire, the device merges the rest itself. That requires the
coordinator's optimistic cache to do the same merge on the read side so a
setpoint change doesn't blank out current/min/max/unit for the settle
window: common.py gains merge_items_field() next to the existing
merge_options_field(), and async_send_command wires it in for any write
touching x.com.samsung.da.items.

New fixture/golden for the cool-only global RAC variant (TP1X_DA-AC-RAC-
01001, AI_RAC_GLOBAL_COOLONLY_3.0); display_light added to the goldens
for boards that gain the new options-based switch. Regenerated against
current main rather than copied from the original PR, since those
goldens had already shifted (current_temperature_c/humidity from issue
#75).
2026-07-27 05:00:06 +00:00
Marc Billow 45931332d4 Rework AIComfort as an HVACMode.AUTO + preset overlay, add unmapped-mode warning (issue #93)
AIComfort isn't a distinct thermodynamic operation like Cool/Dry/Heat --
it's an AI-driven overlay on top of the device's own 'Auto' behavior,
confirmed by A-CAWW-TP2-20-COMMON reporting both 'Auto' and 'AIComfort'
as separate, mutually-exclusive entries in /mode/vs/0's supportedModes.
Modeled the idiomatic HA way instead of a flat _DEVICE_TO_HVAC entry:
hvac_mode reports AUTO and a new 'ai_comfort' preset carries the
distinction. Entered/left only via the preset (writes the primary mode
resource, not the convenient one) -- there's no dedicated HVACMode
value for it, so it's not offered in the hvac_mode dropdown directly.

Also adds a once-per-(href, code) warning log when a device-reported
mode has no entry in the relevant map, so a future gap like this one
surfaces in the log instead of silently vanishing -- the exact failure
mode issue #93 called out ("this class of gap is invisible without
diffing against supportedModes").
2026-07-27 02:01:51 +00:00
Marc Billow 8c3250b163 Map AC's AIComfort mode to HVACMode.AUTO (issue #93)
A-CAWW-TP2-20-COMMON (and likely other CAWW/TP2X-class boards) reports
'AIComfort' as a distinct entry in /mode/vs/0's supportedModes,
alongside 'Auto' (already mapped to HEAT_COOL). _read_modes() silently
drops any code missing from _DEVICE_TO_HVAC, so AIComfort was
unreachable -- one of HA's five other AC hvac_modes, unused by this
device family until now.

The other two gaps in issue #93 are already addressed elsewhere and
not duplicated here:
- Fan-only via the 'Fan' device code, and the WindFree/LongWind/
  NanoSleep preset codes, are covered by the still-open PR #91, which
  replaces the static preset table with a fully dynamic resolver over
  the device's own supportedModes.
- The 'Left_And_Right' -> horizontal swing mapping is already on the
  still-open claude/device-support-issue-75-windfree-ac branch.
2026-07-27 01:43:22 +00:00