Adds [tool.ruff] and [tool.ty] config to pyproject.toml with a curated
ruff rule set (E, F, W, I, UP, B, C4, SIM, RUF, ASYNC, LOG, G, PIE, RET,
PERF, N), pins ruff/ty in requirements-dev.txt, reformats the whole tree
with `ruff format`, and fixes the pre-existing lint and type-check debt
those tools surfaced so both run clean.
Production-code type fixes include: HA's ConfigFlowResult vs. the
generic FlowResult in config_flow.py, narrowing BoundEntity.desc to its
platform-specific subclass (SelectDesc/NumberDesc/SensorDesc/etc.) via
cast() instead of an unchecked annotation, converting HA device_class
strings to their proper enum types, a resolve_registry callback typed
as `object` instead of `DeviceRegistry | None`, and a couple of other
narrow correctness fixes (CA key type validation, an index-out-of-bounds
false positive from an empty-tuple fallback, a bool/dict argument swap).
Test-file fixes are mechanical: narrowing SamsungEntityDescription to
the correct subclass via isinstance()/cast() before accessing
subclass-only fields, and asserting Optional write_fn/unit_fn fields
are set before calling them.
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.
This board reports no oneUiVersion and no modelNum token any existing
family routed on, so it fell back to the global unknown-device CAPABILITIES
set -- exposing only power/child-lock/start-stop-pause/delay/energy/machine
state, with /course/vs/0, /diagnosis/vs/0, and /washer/vs/0 all unbound and
no course/mode select at all (the actual reported gap).
Every one of those resources turns out to already be handled by the shared
laundry machinery: /diagnosis/vs/0 reuses dishwasher.DIAGNOSIS, and
/course/vs/0's cycle select works unmodified through laundry.cycle_options'
existing supportedOptions fallback (this board has no /wm/editcourse/vs/0
at all, so editCourseList never populates). /washer/vs/0 gets a new minimal
AIR_DRESSER_SETTINGS capability (wrinkle_prevent only) rather than reusing
dryer.DRYER_SETTINGS wholesale, since this device never reports
dryLevel/dryTime/dryerType at all and binding them would ship three
permanently-unavailable sensors.
Course codes aren't identified yet (no code->name mapping was reported), so
they render as their raw codes until named in translations, same as
dryer.py's precedent for unidentified codes.