Skip to content

feat: expose AP_SUB_MODE_LABELS for Signature/SP4i fan presets#133

Merged
dahlb merged 1 commit into
dahlb:mainfrom
philjn:feature/sp4i-fixture-and-labels
May 17, 2026
Merged

feat: expose AP_SUB_MODE_LABELS for Signature/SP4i fan presets#133
dahlb merged 1 commit into
dahlb:mainfrom
philjn:feature/sp4i-fixture-and-labels

Conversation

@philjn
Copy link
Copy Markdown
Contributor

@philjn philjn commented May 17, 2026

feat: expose AP_SUB_MODE_LABELS for Signature/SP4i fan presets

Summary

Adds a public AP_SUB_MODE_LABELS: dict[int, str] constant to
blueair_api mapping the four apsubmode integer values observed on
Signature-series air purifiers to their consumer-facing preset labels:

Wire value Label
0 manual_fan
2 auto
3 night
4 eco

The constant is consumed by dahlb/ha_blueair#348 / #261
to expose Signature fan preset modes in Home Assistant. This PR ships
only the library piece (constant + fixture + tests); the
integration change is a separate PR that will pin to the release that
ships from this PR.

No behavior change to any existing device — apsubmode was already
plumbed end-to-end through SHADOW_FIELD_MAP, refresh(), and
set_ap_sub_mode in v1.51.0. This PR adds only the public label
map and regression coverage.

Why a library constant (vs. in ha_blueair)?

Three reasons:

  1. Forward compatibility with Phase 3 (ha_blueair#353) — the
    schema-driven entity discovery work explicitly designs a per-slug
    FieldProfile registry whose value_labels field is exactly this
    dict's shape. Keeping the mapping in blueair_api lets the future
    registry import it directly instead of re-discovering the SP4i
    values.
  2. Single source of truth across consumers. If a future client
    (Home Assistant integration, CLI, dashboard) wants the same
    preset list, they should not each maintain their own copy.
  3. Pattern consistency. MQTT_SENSOR_FIELD_MAP and
    SHADOW_FIELD_MAP already live in device_aws.py; this is the
    same idiom.

Why a constant and not an Enum or Literal type?

The apsubmode shadow slug is polyvalent across device families
(see the constant's docstring). An Enum declaring "the legal values
of apsubmode" would either:

  • be wrong (Signature has 0/2/3/4; T10i uses 1/2; pet_air_pro
    declares the slug but ignores it), or
  • need per-family enum classes, which is more abstraction than the
    current state of the codebase asks for.

A plain dict keyed by int with a long disambiguating docstring keeps
the contract honest: this map is Signature-family only, NOT a
global apsubmode decoder. The docstring spells out the three known
namespaces and how ha_blueair's fan platform keeps them disjoint
via a capability gate (ap_sub_mode != NotImplemented AND fan_auto_mode == NotImplemented AND night_mode == NotImplemented).

What's in this PR

src/blueair_api/device_aws.py

  • New top-level constant AP_SUB_MODE_LABELS with a docstring that
    explains polyvalent-slug pitfalls and Phase 3 forward compatibility.

src/blueair_api/__init__.py

  • Re-exports AP_SUB_MODE_LABELS from the package root so
    downstream consumers can from blueair_api import AP_SUB_MODE_LABELS (stable import contract).

tests/device_info/SP4i.json

  • New fixture derived from a sanitized SP4i device_info response
    shared by @Pazuzu6666 in
    ha_blueair#348.
    All user-identifying tokens (UUIDs, MAC, serial, account ID,
    Wi-Fi credentials, home address, lat/lon, auth tokens) are
    replaced with synthetic placeholders following the same
    convention as the existing T10i.json and pet_air_pro.json
    fixtures.

tests/test_device_aws.py

  • New SP4iTest mirrors T10iTest / Max211iTest. Loads the
    fixture, runs refresh(), and asserts the full attribute set
    via assert_fully_checked. Pinpoints the capability signature:
    • ap_sub_mode == 2
    • fan_auto_mode is NotImplemented
    • night_mode is NotImplemented
    • model_name == "Blueair Blue Signature SP4i" (SKU 112936)
    • mqtt_sensor_slugs == ["pm1", "pm2_5", "pm10", "rssi"]
  • New ModelNameTest.test_sp4i_sku_known covers the SKU table
    entry directly.
  • New ApSubModeLabelsTest (4 tests):
    • test_labels_match_known_signature_mapping — locks the
      canonical 0/2/3/4 → manual_fan/auto/night/eco mapping.
    • test_labels_are_unique — guards against silent reverse-lookup
      breakage.
    • test_labels_are_well_formed — int keys, non-empty string
      values.
    • test_publicly_importable_from_package_root — protects the
      __init__.py re-export against future refactors.

Validation

pytest tests -q
............................................................[ 53%]
............................................................[100%]
135 passed in 3.32s

ruff check src/ tests/
All checks passed!

mypy src/blueair_api/device_aws.py
Success: no issues found in 1 source file

Versioning

Suggest v1.51.1 patch release after merge — additive public API,
no behavior change, no migration concerns.

Related

  • Closes part of (but not all of) dahlb/ha_blueair#348
    integration-side fan-platform change ships in a separate PR
    against dahlb/ha_blueair:main after this lands.
  • Closes part of dahlb/ha_blueair#261 (Signature Modes umbrella).
  • Forward-compat hook for dahlb/ha_blueair#353 (Phase 3
    schema-driven entity discovery) — the registry's
    FieldProfile("apsubmode") will consume this dict as
    value_labels once that work starts.

Adds a public AP_SUB_MODE_LABELS dict mapping the four `apsubmode` values observed on Blueair Blue Signature SP4i (type_name='blue40', hw='l_blue40') to their HA-side preset labels:

  0 -> manual_fan, 2 -> auto, 3 -> night, 4 -> eco

Signature devices declare `apsubmode` but NOT `automode` / `nightmode` in their `configuration.dc` schema, which is the capability signature `ha_blueair` uses to switch on Signature-style preset modes in the fan platform (dahlb/ha_blueair#348, #261).

Also adds a fixture (tests/device_info/SP4i.json, derived from a sanitized SP4i debug log shared on ha_blueair#348) and matching SP4iTest + ModelNameTest.test_sp4i_sku_known + ApSubModeLabelsTest.

No behavior change to existing devices; AP_SUB_MODE_LABELS is a new public constant only. Phase 3 of dahlb/ha_blueair#334 (tracked as ha_blueair#353) will consume this dict as the value_labels of a future FieldProfile('apsubmode').
@dahlb dahlb merged commit 1b0da38 into dahlb:main May 17, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants