Espex.Wizard behaviour (espex v0.12.0)

Copy Markdown View Source

Behaviour for device wizard adapters (API 1.18).

The device wizard is the set of pages Home Assistant shows when the device is added. A page can show entities of this device, for example a switch to test, and ask the user to pick Home Assistant entities, for example the calendars a display should show. The client sends each pick back to the device, and espex hands it to input_set/2.

Without a :wizard adapter espex reports no wizard and ignores the wizard messages, as ESPHome firmware built without one.

Callbacks

CallbackPurpose
wizard/0Return the Espex.Wizard.Document
input_set/2Receive the entity ids the user picked for an input

The document

Build the document with Espex.Wizard.Document.new!/1. It checks the document against ESPHome's limits, so build it once, for example in a module attribute or at application start, and return it from wizard/0.

espex calls wizard/0 once per connection, when the connection is accepted. Clients read the wizard once, when the device is added, so a changed document reaches Home Assistant only when the device is added again.

Strings pass through to Home Assistant verbatim, so they may be translation placeholders such as "[%key:component::light::title%]".

Inputs

Each Espex.Wizard.Input has an id you choose. Its key on the wire is fnv1_hash/1 of that id, the same hash ESPHome uses, and input_set/2 receives that key. Compare against input.key from the built document, or against fnv1_hash("my_input").

espex validates each WizardInputSetRequest as ESPHome does before it calls input_set/2:

  • A request for an unknown key is ignored.
  • A single input takes one entity id of 1-255 bytes containing a ..
  • A multiple: true input takes a comma-separated list. Spaces around each id are trimmed. The whole request is ignored when any id is empty or has no ., or when the ids joined by , are longer than the input's max_length (255 by default).

An ignored request is logged as a warning, without its value, and input_set/2 is not called. The client gets no reply either way.

Every espex input is standalone: it only hands the entity ids to your adapter, which decides what they drive. ESPHome's inputs linked to a homeassistant platform entity need Home Assistant state subscriptions, which espex does not implement.

Nothing to persist

ESPHome keeps the picks in RAM only, and clients are built for that: after every connection, and whenever the user changes a pick, the client sends a WizardInputSetRequest for every input. So input_set/2 is called again after each reconnect, and the adapter need not store the picks across restarts.

OTP 28

The document is compressed with OTP's :zstd module, which OTP 28 added. Espex.Supervisor refuses to start with a :wizard adapter on an older OTP, and available?/0 reports whether this runtime can serve a wizard.

Example

defmodule MyApp.Wizard do
  @behaviour Espex.Wizard

  alias Espex.Wizard.{Document, EntityFilter, Input, Page}

  @impl true
  def wizard do
    Document.new!(
      pages: [
        %Page{
          title: "Calendars",
          inputs: [
            %Input{
              id: "calendars",
              description: "The calendars to show",
              multiple: true,
              entity_filters: [%EntityFilter{domain: ["calendar"]}]
            }
          ]
        }
      ]
    )
  end

  @calendars Espex.Wizard.fnv1_hash("calendars")

  @impl true
  def input_set(@calendars, entity_ids), do: MyApp.Display.show_calendars(entity_ids)
  def input_set(_key, _entity_ids), do: :ok
end

Wiring

Espex.start_link(
  device_config: [name: "display"],
  wizard: MyApp.Wizard
)

Summary

Callbacks

The user picked entity_ids for the input with key. A single input always gets a one-element list. Return {:error, reason} to have espex log a warning; the client gets no reply either way.

Return the wizard document. Called once per connection, at accept.

Functions

Whether this runtime can serve a wizard: true on OTP 28 or newer, which has the :zstd module.

The 32-bit FNV-1 hash (multiply, then xor) of string's code points. The same as ESPHome's fnv1_hash, which keys wizard inputs by the hash of their id.

Callbacks

input_set(key, entity_ids)

(since 0.12.0)
@callback input_set(key :: non_neg_integer(), entity_ids :: [String.t(), ...]) ::
  :ok | {:error, term()}

The user picked entity_ids for the input with key. A single input always gets a one-element list. Return {:error, reason} to have espex log a warning; the client gets no reply either way.

wizard()

(since 0.12.0)
@callback wizard() :: Espex.Wizard.Document.t()

Return the wizard document. Called once per connection, at accept.

Functions

available?()

(since 0.12.0)
@spec available?() :: boolean()

Whether this runtime can serve a wizard: true on OTP 28 or newer, which has the :zstd module.

fnv1_hash(string)

(since 0.12.0)
@spec fnv1_hash(String.t()) :: non_neg_integer()

The 32-bit FNV-1 hash (multiply, then xor) of string's code points. The same as ESPHome's fnv1_hash, which keys wizard inputs by the hash of their id.

iex> Espex.Wizard.fnv1_hash("calendar_input")
1341199854