Skip to content

Add the delegate, context and connection APIs of the callback interface - #562

Closed
ikalchev wants to merge 2 commits into
devfrom
refactor/callback-api
Closed

ikalchev wants to merge 2 commits into
devfrom
refactor/callback-api

Conversation

@ikalchev

@ikalchev ikalchev commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

The callback interface refactor — the APIs

This PR lays the foundation of the setter/getter callback interface refactor: the pure API — classes, contexts and the error protocol — without any integration into the driver's pipelines yet. The two PRs stacked on top make the first pieces of the driver use it.

The bigger picture

The current callback contract grew ad hoc and shows the strain:

The redesign, agreed in discussion:

  • Delegates, not callbacks: CharacteristicDelegate (on_get / on_set / on_subscribe / on_unsubscribe) and ServiceDelegate (grouped on_set), noop-base classes; one instance may serve many characteristics.
  • Frozen contexts (CharContext / CharReadContext / CharWriteContext): client_addr, client_uuid, connection, deadline, is_admin. New input data arrives as new fields, never as signature changes.
  • Async by design: every delegate method is a coroutine function, validated once at registration (a sync override fails with a TypeError); the driver awaits every call under the context's deadline. No sync/async inference anywhere.
  • HapStatusError: callbacks answer a curated HAP status instead of the generic -70402 (closes Allow characteristics to indicate set value failure #75). Reserved codes fail at construction.
  • A grouped-first gate write pipeline: service/accessory on_set runs before per-characteristic ones and may veto via HapStatusError — values restored, nobody notified, nothing applied.
  • A uniform async read path with per-characteristic error isolation; lifecycle hooks (client_connected, client_verified, keep client_disconnected) as overridable Accessory methods.
  • A legacy facade: setter_callback/getter_callback become properties wrapping old callables; the legacy pipeline keeps today's semantics byte-for-byte. Existing integrators change nothing until they choose to.

This PR: API only

Module What
pyhap.errors HapStatusError with the curated status set
pyhap.connection HAPConnectionHandle — equal only for the same connection, never for a reused address; phased internal migration documented in the docstring
pyhap.context CharContext / CharReadContext / CharWriteContext, frozen dataclasses
pyhap.delegates CharacteristicDelegate / ServiceDelegate, noop async bases

The driver does not dispatch any of this yet. Unit tests cover construction, equality/hash, frozenness, coroutine-function defaults and overrides; autodoc pages added under docs/source/api/. The handle exposes only client_addr — what makes it unique (the address plus an internal generation) is an implementation detail. CHANGELOG entries come with the finished design.

Stack: #560 (write path answers HapStatusError) → #561 (read path + per-characteristic isolation + the busy_lock example).

A callback that fails answers -70402 (service communication failure),
whatever went wrong: a busy device, a refused request and a broken one
look the same to the controller. This is the error protocol of the
callback interface refactor (refactor.md); issue #75 asked for it.

HapStatusError carries a HAP status the controller receives for the
characteristic, e.g. RESOURCE_BUSY for a lock whose motor is moving. The
status set is curated: privileges, permissions and timeouts belong to
the accessory driver and cannot be raised, a reserved or unknown status
fails at construction with a ValueError.
Comment thread pyhap/connection.py

__slots__ = ("_client_addr", "_generation")

def __init__(self, client_addr: tuple[str, int] | None, generation: int) -> None:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When can this be None?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented in the constructor docstring now: None only for a call without a connection — a programmatic write/read through the legacy facade, e.g. tests calling client_update_value directly. A controller's request always has an address.

Comment thread pyhap/connection.py Outdated
Comment on lines +48 to +51
@property
def generation(self) -> int:
"""What distinguishes a connection from a reused address."""
return self._generation

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that really need to be exposed? The key is what makes a handle unique and this is just an implementation detail IMO.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — dropped the public generation property. It stays a driver-assigned constructor parameter, documented as internal; equality and hashing still use it. The handle's interface is now just client_addr.

Comment thread pyhap/context.py Outdated


@dataclass(frozen=True)
class Context:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name is somewhat generic. Perhaps CharContext? CharReadContext and CharWriteContext respectively.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to CharContext / CharReadContext / CharWriteContext across the modules, tests and docs. The prefix also keeps room for the lifecycle contexts later, which are not char-shaped.

Comment thread CHANGELOG.md Outdated

## [Unreleased]

### Added

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop the changes to the changelog for now since the design is still in a very early draft.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped.

The API side of the callback interface refactor, ahead of the write and
read pipelines that dispatch it:

- CharacteristicDelegate and ServiceDelegate: the noop-base interfaces
  replacing the legacy setter/getter attributes over time. Every method
  is a coroutine function by design - the driver awaits every call,
  there is no sync/async detection on this path - and a delegate
  instance may serve many characteristics: the context carries which
  one a call is about.
- Context, ReadContext and WriteContext: frozen dataclasses carrying
  what a callback knows about the request - the client's address and
  identity, the connection handle and the deadline - so new input data
  arrives as new context fields instead of new callback signatures.
- HAPConnectionHandle: the stable identity of a controller connection,
  equal only for the same connection, never for a later one reusing its
  address. Camera session ownership adopts it first; the driver's
  remaining per-client state migrates in a second phase, documented in
  the class docstring.

The driver does not dispatch these yet; they are pure API with unit
tests and autodoc pages. HapStatusError from the previous commit rides
along as the error protocol of the same interface.
@ikalchev

Copy link
Copy Markdown
Owner Author

Closing per maintainer decision — continuing locally; the work continues on local branches and will return as PRs when the design settles.

@ikalchev ikalchev closed this Nov 13, 2026
@ikalchev
ikalchev deleted the refactor/callback-api branch November 13, 2026 20:18
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.

Allow characteristics to indicate set value failure

1 participant