b8ef430ed4a72daa05bf052d087cfb11a0e9a7b8
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
daf7e3787f |
Add ruff (lint + format) and ty (type checking) to the project
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. |
||
|
|
9e85395b44 |
rename: unit/sub-unit -> subdevice, matching OCF terminology
"Unit"/"sub-unit" from #199's multi-indoor-device support wasn't OCF idiomatic -- OCF calls each component of a composite device a "subdevice" (see subdeviceIdList), so rename SubUnit -> Subdevice throughout: the registry module, coordinator state, BoundEntity's subdevice field, diagnostics keys (subdevices/subdevices_skipped/ subdevice_probes), entity unique_id prefixes (unit1_/sub_<uuid>_ -> subdevice1_/subdevice_<uuid>_), device-name fallback labels, golden fixtures, tests, README, and the adding-device-support skill. Breaking change to entity unique_ids and diagnostics keys, acceptable since this hasn't been released yet. |
||
|
|
c181a8b447 |
feat(subdevices): support multi-indoor-unit systems (#177)
Samsung 2-in-1 air conditioners put more than one logical indoor unit behind a single IP and a single DTLS session. Only the unit the config entry was set up against was ever discovered; the second one -- a whole physical appliance the user can see in SmartThings -- had no entities at all. Two reporters turned out to have two different mechanisms: ARTIK051_DONGLE_FAC_18K -- indexed siblings. /oic/res registers the whole tree discoverable and lists three complete parallel resource sets whose trailing path segment is the index (/mode/vs/0, /mode/vs/1, ...), on OCF-standard and vendor hrefs alike. /device/0's batch carries only the index-0 hrefs, so a sibling is reachable only through its own /device/<n> collection. TP2X_FAC_BORA_21K -- UUID-prefixed tree. /oic/res hides the appliance tree entirely (which is why a direct /device/1 probe returns nothing on this board). /subdevices/vs/0 carries subdeviceIdList instead, and that UUID appears as a literal href prefix; /<uuid>/information/vs/0 was confirmed live to return the wall unit's own model and serial (TP2X_FAC_BORA_RAC_21K) against the master's TP2X_FAC_BORA_21K. The detection signals don't overlap on either board, so no disambiguation is needed -- enumeration checks both and takes what answers. Both patterns are the same thing underneath: a logical unit is a seed collection path to poll plus an href transform between the canonical href the registry knows and the actual on-the-wire href. That is the whole abstraction (SubUnit), applied at four boundaries -- discovery, the coordinator, the adapter, and the platforms. Capabilities, the registry and the climate composite stay written against canonical hrefs and are untouched. Uniqueness comes from a key_prefix inside the flattened state key, so the master unit's keys are byte-identical to every release before this and every existing golden file is an unchanged regression guard. Each sub-unit gets its own device-registry entry linked by via_device and named from its own /information/vs/<n>, so it lands in its own room rather than crowding the master's device page. A sub-unit materializes only when it yields at least one primary (non-diagnostic) entity with a populated value. That gate is not decoration: the reporter's /device/2 is an unused slot that SmartThings shows disabled, yet it answers with a full 14-href batch, and it flattens to exactly one non-None value -- a diagnostic alarm_code derived from an empty /alarms/vs/2. Without the entity-category filter it becomes a phantom third climate card. The rule is deliberately domain-agnostic rather than a list of HVAC hrefs, so a multi-drum washer (#19) gets the same treatment with no new curation. Units that answer but fail the gate are logged and reported in diagnostics, so a genuinely missing unit stays diagnosable from a dump. Enumeration fetches things that must not then be treated as appliance state. A rejected candidate's seed has to be read to evaluate the gate, but only units that pass are polled again, and StateCache has no eviction -- so discovery runs before the first cache apply and those reps are held aside for diagnostics rather than frozen into the cache forever. /multidevice/vs/0 is probed on every device regardless of family, so merging it into the resources dict would have reached discovery on any board whose registry doesn't ignore that href -- only the air conditioner one does -- raising a spurious coverage-gap repair for a washer or fridge whose firmware answers it. It is corroborating metadata (numofsubdevice, confirmed read-only) and now lives beside the resources rather than in them. Diagnostics reports each unit separately: top-level `resources` is this unit's own and only its own, which is what the module docstring and the adding-device-support skill have always claimed it was, and each sibling or rejected candidate carries its own reps canonicalized so a block reads exactly like the master's instead of needing to be de-indexed by hand. Fixtures are real captures. The ARTIK051_DONGLE_FAC_18K one is entirely verbatim, both sibling seeds and the hand-read /multidevice/vs/0 included. The TP2X_FAC_BORA one has a real device0, oic_res and sub-unit /information/vs/0, with the remainder of that unit's tree constructed and documented as such in seeds_note; /<uuid>/device/0 is the one part of that pattern still inferred rather than observed, and can't be tested through the debug panel because a Collection returns a list. |
||
|
|
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.
|
||
|
|
fcd34f73e5 |
Add device support for BESPOKE Cube Air A-VTWW-TP2-21-COMMON (#151)
This board reports no oneUiVersion and no modelNum token any existing
family routed on, so it fell back to unknown -- exposing nothing but power
even though most of its resources (air quality sensors, HEPA filter,
device-active, diagnosis, plumbing hrefs) are already handled generically
by the existing air_purifier registry via the '-VTWW-' modelNum fallback.
The one genuinely new piece is its fan: this board reports wind strength as
numeric codes ("87"/"89"/"90"/"91") on /wind/strength/vs/0 with a separate
modesName array ("SMART"/"MAX"/"WINDFREE"/"Sleep") giving the actual names,
unlike the existing TP1X_DA-AC-AIR family where supportedModes IS the name
list already. Generalized LocalThingsAirPurifierFan to resolve a mode code
through modesName when present (same live-label pattern as climate.py's AC
wind-strength fix, issue #155) instead of adding a second hardcoded fan
class, and added WIND_STRENGTH_FAN reusing the same 'air_purifier_fan'
translation catalog -- both board generations land on the identical
smart/max/windfree/sleep vocabulary already labelled there.
/mode/convenient/vs/0 is empty on this dump and added to COVERAGE alongside
the existing plumbing hrefs this board also shares with the TP1X_DA-AC-AIR
family.
|