# API 1.18

espex 0.12.0 advertises API 1.18. This page covers what 1.18 added and
how espex implements each part. Every feature is optional: configure
its adapter or option, or leave it out.

## What's new

| Feature | Messages | espex |
|---------|----------|-------|
| IR/RF transmit completion | `InfraredRFTransmitCompleteResponse` | `Espex.InfraredProxy` |
| Sendspin | `DeviceCapabilitiesResponse.sendspin`, `SendspinPairingTokenRequest` | `Espex.Sendspin` |
| Serial proxy identity | `SubscribeSerialProxyIdentityRequest`, `SerialProxyIdentity` | `c:Espex.SerialProxy.identity/1` |
| Device wizard | `DeviceWizardRequest`, `WizardInputSetRequest` | `Espex.Wizard` |
| Home Assistant states | `SubscribeHomeAssistantStatesRequest`, `SubscribeHomeAssistantStateResponse`, `HomeAssistantStateResponse` | `Espex.HomeAssistantStates` |
| Wizard inputs linked to a state | the two above together | `links` on `Espex.Wizard.Input` |
| Dial-back | `HelloRequest.outgoing_connection_target`, `DeviceInfoResponse.api_outgoing_connection_supported` | `:outgoing_connection`, `Espex.OutgoingTargetStore` |

## What espex implements

### IR/RF transmit completion

A client on 1.18 sends an IR/RF frame and waits for
`InfraredRFTransmitCompleteResponse` before the next one (or 35 s past
the frame's air time). espex answers with the return of
`c:Espex.InfraredProxy.transmit_raw/3`, which runs in a task, off the
connection, so the connection keeps serving other requests meanwhile.
The rules follow ESPHome firmware:

| Situation | Reply |
|-----------|-------|
| `transmit_raw/3` returns `:ok` | `success: true` |
| It returns `{:error, _}`, raises or exits | `success: false` |
| No adapter, no timings, or a key `list_entities/0` did not advertise | `success: false` at once, adapter not called |
| A newer request for the same key, from any client | `success: false` for the older one at once; if it had not started it is dropped, if it is running it finishes |
| No return 30 s past the frame's air time (air time counted up to 8 min) | `success: false`; the call is killed and the next frame starts |
| The requester disconnected | none; its frames still go out |
| The client is below API 1.18 | none, refusals included; any newer request for the key drops its frame if not yet started |

Calls run one at a time per adapter module, in arrival order, and in
parallel across modules. A request runs on the adapter its connection
started with.

### Sendspin

With an `Espex.Sendspin` adapter, `DeviceCapabilitiesResponse` carries
its `client_id/0` and feature flags, read on every request, and
`SendspinPairingTokenRequest` is answered from `pairing_token/0`. The
token goes over Noise only; a plaintext session gets
`ENCRYPTION_REQUIRED`.

### Serial proxy identity

`SubscribeSerialProxyIdentityRequest` is answered with one
`SerialProxyIdentity` per advertised port, from the optional
`c:Espex.SerialProxy.identity/1`. Later changes go to subscribed
clients with `Espex.push_serial_proxy_identity/2`.

### Device wizard

With an `Espex.Wizard` adapter (OTP 28 or newer), capabilities report a
wizard, `DeviceWizardRequest` is answered with the
`Espex.Wizard.Document` compressed as the protocol requires, and each
valid `WizardInputSetRequest` reaches `c:Espex.Wizard.input_set/2`.

### Home Assistant states

The device keeps a table of `Espex.HomeAssistantStates.Subscription`
entries (an entity id and an optional attribute). On
`SubscribeHomeAssistantStatesRequest` espex sends one
`SubscribeHomeAssistantStateResponse` per entry; Home Assistant then
sends each entry's state as it changes, and espex hands it to
`c:Espex.HomeAssistantStates.handle_state/3`.

Entries come from `c:Espex.HomeAssistantStates.subscriptions/0`, from
`Espex.subscribe_home_assistant_state/2` at runtime, and from linked
wizard inputs. An entry asked for twice is sent once. A client that has
already subscribed is sent new entries as they appear; removing one
sends nothing, as the protocol has no unsubscribe.

### Wizard inputs linked to a state

An `Espex.Wizard.Input` with `links` subscribes to the entity the user
picks. Setting the input replaces its entries in the table, and every
client that subscribed to states is sent the new ones. Unlike firmware,
setting the value an input already has sends nothing (firmware resends,
which makes Home Assistant track the entity twice). The wizard JSON has
no field for links; they stay on the device.

### Dial-back

With `:outgoing_connection`, the node dials Home Assistant's dial-back
listener (port 6054) when no client is connected, for networks where
Home Assistant cannot reach it. The dialed connection sends its Noise
hello first and is otherwise a normal connection. The target is a
configured IP address, or else learned from the client whose hello sets
`outgoing_connection_target` and kept through an
`Espex.OutgoingTargetStore`. The timing rules (first dial after
`delay`, no dial while a target client is connected, 5 s backoff
doubling to 300 s) are under "Dial-back" in `Espex.Supervisor`.

## Usage

    defmodule MyApp.HAStates do
      @behaviour Espex.HomeAssistantStates

      alias Espex.HomeAssistantStates.Subscription

      @impl true
      def subscriptions, do: [Subscription.new!(entity_id: "sun.sun", attribute: "elevation")]

      @impl true
      def handle_state(entity_id, attribute, state) do
        send(MyApp.Display, {:ha_state, entity_id, attribute, state})
        :ok
      end
    end

    defmodule MyApp.Wizard do
      @behaviour Espex.Wizard

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

      @impl true
      def wizard do
        Document.new!(
          pages: [
            %Page{
              title: "Outdoor temperature",
              inputs: [
                %Input{
                  id: "outdoor",
                  entity_filters: [%EntityFilter{domain: ["sensor"]}],
                  links: [[]]
                }
              ]
            }
          ]
        )
      end

      @impl true
      def input_set(_key, _entity_ids), do: :ok
    end

    target = MyApp.TargetStore.load()

    Espex.start_link(
      device_config: [name: "my-device", psk: MyApp.Keys.psk()],
      infrared_proxy: MyApp.IRBlaster,
      sendspin: MyApp.Sendspin,
      wizard: MyApp.Wizard,
      home_assistant_states: MyApp.HAStates,
      outgoing_connection: if(target, do: [target: target], else: []),
      outgoing_target_store: MyApp.TargetStore
    )

    # Later, at runtime:
    :ok = Espex.subscribe_home_assistant_state("weather.home")

## Upgrade notes

* **The Noise server hello carries the bare MAC.** It is now 12
  lowercase hex digits (`"aabbccddeeff"`) on every connection, as
  firmware sends it; Home Assistant's dial-back listener requires that
  form. `DeviceInfoResponse.mac_address` keeps the configured form. A
  tool of your own that read the colon form from the hello must change.
* **IR/RF transmits no longer block the connection.** They run in a
  task, so `c:Espex.InfraredProxy.transmit_raw/3` may run while the
  same connection handles other messages, and may run at the same time
  as calls on other adapter modules.
* **Long IR/RF frames are no longer refused.** espex used to refuse a
  frame of more than 30 s on air. It now runs it, and kills the call
  30 s past the frame's air time (air time counted up to 8 minutes).
* **Clients below API 1.18 get no transmit reply.** espex used to reply
  to every client; it now replies as firmware does.
* **IR/RF requests for an unknown key are refused** without calling
  the adapter.
* **thousand_island 1.5 or newer is required**, for dial-back.
