Skip to content

[bot] Add CredentialsOption and the button credentials type - #6

Merged
MarkParker5 merged 5 commits into
developfrom
feature/credentials-option
Oct 14, 2026
Merged

MarkParker5 merged 5 commits into
developfrom
feature/credentials-option

Conversation

@MarkParker5

@MarkParker5 MarkParker5 commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

For review, do not merge yet.

Problem

  • CredentialsType.with_mask() set code_mask on the single shared enum member, so the last mask won for every discovery.
  • The mask was never serialized: expected_credentials_options dumped only "code".
  • No shared way for an integration to say the Hub itself lacks credentials a pairing needs (e.g. a Hub on Ethernet has no Wi-Fi to give an ESPHome node set up over BLE).

API diff

  • New CredentialsOption(Base): type: CredentialsType, mask: str | None = None, hint: str | None = None (exported from schemas).
  • Discovery.expected_credentials_options: list[CredentialsType] becomes list[CredentialsOption]. Wire form: [{"type": "code", "mask": "DDDD-DDD-DDDD", "hint": null}] (unset fields dump as null, the pydantic default).
  • New Discovery.accepts(credentials: ProvidedCredentials | None) -> bool; no credentials count as none.
  • New CredentialsType.button: the user physically confirms on the device (ESPHome Improv authorizer, Matter/HomeKit setup button). A confirmation step with no value, distinct from none (nothing asked of the user). ProvidedCredentials(type=button) validates with no value.
  • CredentialsType.with_mask(mask, hint=None) and new CredentialsType.with_hint(hint) are the short form: they return a new CredentialsOption and never mutate the shared enum member. The with_mask parameter is renamed code_mask -> mask (all known callers pass it positionally). Both together: CredentialsType.code.with_mask("DDD-DD-DDD", hint="On the box"). CredentialsOption stays a plain data model without builders.
  • Dev CLI discoveries prints o.type.value.
  • New MissingCredentialsError(type: str = "wifi") (a LookupError, exported from the package root, module errors):
    • type is a lower_snake_case string (validated, ValueError otherwise), kept open so an integration can add its own and document it.
    • Known types as constants: MissingCredentialsError.WIFI = "wifi", MissingCredentialsError.THREAD = "thread".
    • Wire form: the Hub answers the pairing request with 422 {type: "missing_credentials", msg: "Missing credentials: <type>", device_id, credentials_type} (Hub PR #149); the message is "Missing credentials: <type>".
    • The iOS app mirrors the type as an enum to pick the screen to show. To be documented in the docs repo.

Compatibility

  • Before-validator on expected_credentials_options accepts bare CredentialsType or str items, so stored discoveries (["code"]) and older integrations ([CredentialsType.none]) still validate.
  • CredentialsOption.__eq__ matches a bare type/str of the same type, so existing credentials.type in discovery.expected_credentials_options checks (matter, homekit, esphome) keep working unchanged.
  • Breaking for consumers that read the items as enums (e.g. .value on an item, or a Hub/iOS client expecting a list of strings).
  • MissingCredentialsError is additive.

Release order

  1. Merge this PR, release the SDK.
  2. Bump majordom-integration-sdk in integration-esphome#9 to that release (it raises MissingCredentialsError), then merge it.

Tests

  • tests/test_credentials.py (17 tests): mask round-trip, legacy bare types (enum and str, and from stored JSON), with_mask not mutating and emitting no warning, with_hint, with_mask(mask, hint=...) with serialization, each call returning a new option, CredentialsOption having no builders, in compat, accepts, button without a value.
  • tests/test_errors.py (red first): default and known types, message, custom type, lower_snake_case validation, LookupError.
  • poe check --ci green locally on Python 3.13 (116 passed, ruff, ty, build, poetry check).

Note: spec_drift.py formatting on develop is fixed separately in fix/ci-gate; it is not in this diff.

- Discovery.expected_credentials_options carries type, mask and hint;
  bare types still validate and still match `in` checks
- Discovery.accepts() checks provided credentials against the options
- Deprecate CredentialsType.with_mask(); it no longer mutates the enum
@MarkParker5
MarkParker5 merged commit 49a2492 into develop Oct 14, 2026
6 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.

1 participant