Skip to content

Advertise ESPHome API 1.18: IR/RF transmit completion, Sendspin, serial identity, device wizard - #44

Merged
bbangert merged 10 commits into
mainfrom
feat/api-1-17
Oct 20, 2026
Merged

bbangert merged 10 commits into
mainfrom
feat/api-1-17

Conversation

@bbangert

Copy link
Copy Markdown
Owner
  • Sync priv/protos to esphome/esphome@0a501b7ef and regenerate lib/espex/proto; register message ids 153-160
  • Answer every InfraredRFTransmitRawTimingsRequest with InfraredRFTransmitCompleteResponse (key and device_id echoed, requesting connection only). Success is the return of transmit_raw/3; no adapter, no timings or more than 30 s on air is refused with success: false
  • New optional Espex.Sendspin adapter (:sendspin): fills DeviceCapabilitiesResponse.sendspin per request and answers SendspinPairingTokenRequest, with the token sent only over Noise (ENCRYPTION_REQUIRED on plaintext) and never logged
  • Serial proxy identity: Espex.SerialProxy.Identity, optional identity/1 callback, Espex.push_serial_proxy_identity/2; SubscribeSerialProxyIdentityRequest answered per advertised port, then on each pushed change. SerialProxy.Info.port_type gains :usb_serial
  • New optional Espex.Wizard adapter (:wizard, OTP 28+ for :zstd): Espex.Wizard.Document builder validated against ESPHome's limits, JSON+zstd blob, Espex.Wizard.fnv1_hash/1 input keys, WizardInputSetRequest validated as api_wizard.cpp, wizard.configured capability
  • Espex.Supervisor and Espex.update_adapters/2 accept :sendspin and :wizard; a :wizard adapter is refused without :zstd
  • @api_version_minor 18, version 0.12.0; README, architecture and entity-types guides updated (missing_state on switch, climate, water heater)
  • Remove HANDOFF-sendspin.md

🤖 Generated with Claude Code

claude and others added 8 commits October 20, 2026 00:42
Regenerates lib/espex/proto from esphome/esphome@0a501b7ef (API 1.18 on
the firmware side). Every change is additive.

New in the schema and now decodable:
- InfraredRFTransmitCompleteResponse (153)
- SubscribeSerialProxyIdentityRequest (154), SerialProxyIdentity (155),
  UsbDeviceDescriptor, and the identity source and flag enums
- DeviceWizardRequest (156), DeviceWizardResponse (157),
  WizardInputSetRequest (158), DeviceCapabilitiesResponse.wizard
- SendspinPairingTokenRequest (159), SendspinPairingTokenResponse (160),
  DeviceCapabilitiesResponse.sendspin
- HelloRequest.outgoing_connection_target and
  DeviceInfoResponse.api_outgoing_connection_supported
- SERIAL_PROXY_PORT_TYPE_USB_SERIAL
- missing_state on switch, climate and water heater state responses

The new progmem field option is C++-only; on the Elixir side it adds
the extension and a deprecated: false on the fields that carry it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aioesphomeapi paces IR/RF frames on this reply once a device reports
API 1.18, and waits 35 s past each frame's air time without it, so
espex must send it before advertising 1.18.

- The completion is the return of transmit_raw/3: :ok reports
  success: true, {:error, _} success: false. It echoes key and
  device_id and goes only to the requesting connection.
- A request with no infrared adapter or no timings is refused at once
  with success: false, without calling the adapter.
- The :infrared_transmit action carries device_id, and its type names
  InfraredProxy.transmit_opts() instead of SerialProxy.open_opts().
- InfraredProxy documents when transmit_raw/3 should return and that it
  blocks the connection for the frame's air time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host running a Sendspin client reports it through a :sendspin
adapter. espex fills DeviceCapabilitiesResponse.sendspin and answers
SendspinPairingTokenRequest (API 1.18) from it.

- Espex.Sendspin: client_id/0, pairing_token/0, and an optional
  feature_flags/0 that defaults to the PAIRING_TOKEN bit.
- The adapter is read on every request, because the client id and token
  only exist once the Sendspin client has started.
- The token goes only over Noise. A plaintext connection, or one still
  in the handshake, gets ENCRYPTION_REQUIRED and the adapter is not
  asked.
- Without an adapter, capabilities carry no sendspin and the token
  request is not answered, as firmware built without Sendspin.
- The token never reaches a log. A crashing adapter or an unexpected
  return is logged by shape only and answered FAILED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At API 1.18 a client subscribes to the identity of the device behind
each serial proxy port, to tell identical devices apart.

- Espex.SerialProxy.Identity describes a port: source :none or :usb,
  the connected and error flags, the descriptor strings and the USB
  ids. The USB descriptor is sent only for :usb.
- An optional identity/1 callback on Espex.SerialProxy supplies it.
  Without the callback, or when it returns nil, the port reports NONE.
- A subscribe is answered with one SerialProxyIdentity per advertised
  port. Without a serial adapter it goes unanswered, as firmware
  without the serial proxy.
- Espex.push_serial_proxy_identity/2 broadcasts a change. Each
  connection forwards it only if it subscribed and advertises the
  instance.
- SerialProxy.Info.port_type gains :usb_serial.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The device wizard is the pages Home Assistant shows when a device is
added (API 1.18). A page can show entities of the device and ask the
user to pick Home Assistant entities, which the client sends back.

- Espex.Wizard: wizard/0 returns an Espex.Wizard.Document, and
  input_set/2 receives the picked entity ids by input key.
- Document.new/1 builds pages of Entity and Input structs, with
  EntityFilter on inputs, and checks them against ESPHome's limits:
  string lengths, filter fields, max_length, unique input keys, and a
  compressed size that fits one API message.
- The wire blob is compact version 1 JSON in one zstd frame, with empty
  values left out, as wizard.py builds it.
- Input keys are Espex.Wizard.fnv1_hash/1 of a host-chosen id, the same
  FNV-1 over code points ESPHome uses.
- The document is captured when a connection is accepted. Capabilities
  then report wizard.configured, and DeviceWizardRequest gets the blob.
- WizardInputSetRequest is validated as api_wizard.cpp does: unknown
  keys and invalid values are ignored with a warning that leaves out
  the value. Spaces around the ids of a multiple input are trimmed.
- Without a wizard adapter the capability is unset and both requests
  are ignored.
- :zstd needs OTP 28. Espex.Supervisor refuses a :wizard adapter on an
  older OTP, and so does Espex.update_adapters/2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every 1.18 message a client relies on is now handled: IR/RF transmit
completion, serial proxy identity, and the optional Sendspin and device
wizard messages. Version 0.12.0.

- @api_version_minor 18, with a 1.18 entry in the api_version_minor/0
  version history.
- README: API 1.18 features, Espex.Sendspin and Espex.Wizard.
- architecture guide: every adapter option in the configuration table.
- entity types guide: missing_state on switch, climate and water heater.
- @moduledoc since: "0.12.0" on the new modules.
- Document.invalid/1 is specced no_return for dialyzer.
- HANDOFF-sendspin.md is removed; the work it planned is done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A Sendspin pairing token that is not valid UTF-8 is answered FAILED.
  protobuf refuses such a string with the bytes in its error, which put
  the token in the connection's crash log. An empty token is FAILED too.
- A crashing or ill-typed client_id/0 or feature_flags/0 reports an
  empty client id instead of closing the connection, since clients read
  capabilities on every connect.
- An IR transmit whose timings times repeats exceed 30 s on air is
  refused with success: false, so one request cannot hold the
  connection for hours.
- A WizardInputSetRequest value over 4095 bytes is refused before it is
  split.
- The wizard blob limit is 65511 bytes: the Noise frame size also
  covers the response's tag and length.
- Tests check that nothing more was sent with a ping round trip instead
  of a 200 ms timeout, and check that the token stays out of the log on
  the OK and plaintext paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Wizard normalization and filter validation defects remain, along with missing Sendspin token size enforcement.

3 open issues
What changed in this PR

Extends espex’s ESPHome Native API support to 1.18 with new adapter capabilities and protocol messages.

Changes:

  • Adds IR/RF transmit completion, Sendspin pairing, serial identity subscriptions, and device wizards.
  • Updates protobuf definitions, message registration, and adapter configuration.
  • Adds coverage and documentation for the 0.12.0 release.
File Description
test/​support/​tcp_client.ex Adds a no-response assertion helper.
test/​support/​fake_adapters.ex Adds adapters for new feature tests.
test/​espex/​wizard_test.exs Tests wizard validation and encoding.
test/​espex/​server_test.exs Tests new adapter registration.
test/​espex/​server_state_test.exs Updates adapter defaults.
test/​espex/​serial_proxy_identity_test.exs Tests identity encoding.
test/​espex/​encrypted_integration_test.exs Tests encrypted pairing and secret-safe logging.
test/​espex/​dispatch_test.exs Tests new request and event routing.
test/​espex/​connection_state_test.exs Updates adapter defaults.
test/​espex/​api_1_18_test.exs Adds TCP integration coverage.
test/​espex/​api_1_17_test.exs Allows newer advertised API versions.
README.md Documents features and release dependency.
priv/​protos/​api.proto Synchronizes protocol messages and fields.
priv/​protos/​api_options.proto Adds the upstream flash-storage option.
mix.exs Bumps version to 0.12.0.
lib/​espex/​wizard/​page.ex Defines wizard pages.
lib/​espex/​wizard/​input.ex Defines inputs and validates selections.
lib/​espex/​wizard/​entity.ex Defines wizard entity references.
lib/​espex/​wizard/​entity_filter.ex Defines entity-selection filters.
lib/​espex/​wizard/​document.ex Validates and compresses wizard documents.
lib/​espex/​wizard.ex Defines wizard callbacks and hashing.
lib/​espex/​supervisor.ex Accepts adapters and checks wizard availability.
lib/​espex/​server_state.ex Validates runtime adapter updates.
lib/​espex/​serial_proxy/​info.ex Adds USB serial port type.
lib/​espex/​serial_proxy/​identity.ex Defines and encodes port identities.
lib/​espex/​serial_proxy.ex Adds optional identity callback.
lib/​espex/​sendspin.ex Defines Sendspin adapter callbacks.
lib/​espex/​proto/​api.pb.ex Regenerates protocol bindings.
lib/​espex/​proto/​api_options.pb.ex Regenerates option bindings.
lib/​espex/​message_types.ex Registers message IDs 153–160.
lib/​espex/​infrared_proxy.ex Documents transmit completion semantics.
lib/​espex/​dispatch.ex Routes new requests and validates transmits.
lib/​espex/​device_config.ex Advertises API 1.18.
lib/​espex/​connection.ex Executes new adapter operations and responses.
lib/​espex/​connection_state.ex Tracks wizard and identity subscription state.
lib/​espex.ex Exposes serial identity broadcasting.
guides/​entity_types.md Documents wizard references and missing states.
guides/​architecture.md Expands adapter configuration documentation.

Review effort: Balanced

Comment thread lib/espex/connection.ex Outdated
Comment thread lib/espex/connection.ex Outdated
Comment thread lib/espex/wizard/document.ex Outdated
- A Sendspin pairing token over 127 bytes (api.proto max_data_length)
  is answered FAILED, without logging it.
- A %Wizard.Document{} returned without Document.new/1 is checked and
  normalised at accept. Its input keys were nil, so every input set for
  it was ignored as an unknown key.
- Wizard filter lists reject entries that are not strings; nil passed
  as optional text and encoded as null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Protocol interoperability and secret-handling changes need human validation, and a confirmed response-size defect remains unresolved.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Reject client IDs exceeding the 43-byte protocol limit

lib/​espex/​connection.ex:1335

This accepts arbitrarily large UTF-8 client IDs despite the 43-byte maximum in api.proto. A 65,536-byte adapter return makes the capabilities response exceed the 16-bit Noise frame lengths, producing a malformed encrypted response instead of the empty-ID fallback. Reject IDs over 43 bytes here, and add regression tests for the 43-byte boundary and oversized returns.

3 resolved since last review

Review effort: Balanced

A client_id longer than api.proto's max_data_length is reported empty
with a warning, like any other unexpected return, instead of being
sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Cross-cutting protocol and secret-handling changes need human interoperability validation because runtime tests were not executed.

No open issues 🎉

Review effort: Balanced

@bbangert
bbangert merged commit 9032338 into main Oct 20, 2026
8 checks passed
@bbangert
bbangert deleted the feat/api-1-17 branch October 20, 2026 22:29
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.

3 participants