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 | 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
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
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 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
Espex.HomeAssistantStates.handle_state/3.
Entries come from 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_addresskeeps 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
Espex.InfraredProxy.transmit_raw/3may 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.