Espex

ESPHome Native API server library for Elixir.

Espex implements the ESPHome Native API protocol over TCP, letting an Elixir application expose itself as an ESPHome device to clients like Home Assistant. The protocol layer and connection lifecycle live here; hardware is plugged in through behaviours.

Status

Early extraction from universal_proxy. Still being tested and iterated on, this is not a final API.

Documentation

Start here once you're ready to go beyond the quickstart below:

Each adapter behaviour's module doc contains a callback reference and a complete example:

Features

Installation

Add to your mix.exs:

def deps do
[
{:espex, "~> 0.1"}
]
end

Usage

Plaintext (no encryption):

Espex.start_link(
device_config: [name: "my-device", friendly_name: "My Device"],
serial_proxy: MyApp.MySerialAdapter,
zwave_proxy: MyApp.MyZWaveAdapter,
infrared_proxy: MyApp.MyInfraredAdapter,
entity_provider: MyApp.MyEntities
)

Encrypted (Noise_NNpsk0) — set :psk to either a 32-byte raw binary or a base64-encoded string matching the format used in ESPHome YAML:

Espex.start_link(
device_config: [
name: "my-device",
friendly_name: "My Device",
psk: "foIclFXDcBlfzi9oQNegJz/uRG/sgdIc956pX+GrC+A="
],
entity_provider: MyApp.MyEntities
)

When a PSK is configured, plaintext clients are rejected with the standard "encryption required" signal so Home Assistant's ESPHome integration prompts the user for the key. Any adapter key you omit disables that feature.

Runtime key provisioning and rotation

Home Assistant can also set the PSK at runtime via NoiseEncryptionSetKeyRequest. Two flows are supported:

A provisioned key takes effect on the next connection (each connection copies the PSK at accept time; HA reconnects automatically). Pass a Espex.PskStore module as :psk_store to persist the key so it survives a restart — without one, espex applies the key to the running server only and logs a warning:

Espex.start_link(
device_config: [name: "my-device", accepts_key_provisioning: true],
psk_store: MyApp.FilePskStore
)

Connected clients

Enumerate the live native-API connections at any time — handy for a "connected clients" dashboard:

Espex.connected_clients(MyApp.EspexServer)
#=> [%Espex.ClientInfo{
#=> peer: "192.168.1.5:54312",
#=> client_info: "Home Assistant 2026.1.0",
#=> api_version: {1, 10},
#=> encrypted?: true,
#=> connected_at: 1_750_000_000,
#=> last_activity_at: 1_750_000_042
#=> }]

To be notified when the set changes (instead of polling), pass a connection_listener module implementing Espex.ConnectionListener. Its connections_changed/0 fires once per connect and once per disconnect; on receipt, re-read connected_clients/1. Start the listener before the Espex child (:rest_for_one) and reconcile on boot — the callback is best-effort, while connected_clients/1 is always the source of truth:

children = [
{MyApp.ClientTracker, []},
{Espex,
server_name: MyApp.EspexServer,
device_config: [name: "my-device"],
connection_listener: MyApp.ClientTracker}
]
Supervisor.start_link(children, strategy: :rest_for_one)

mDNS advertising

Advertise the server as a _esphomelib._tcp service so ESPHome clients auto-discover it. Add :mdns_lite to your application's deps (it's not a runtime dep of espex) and wire the shipped adapter:

# in your mix.exs
{:mdns_lite, "~> 0.8"}
# at start
Espex.start_link(
device_config: [name: "my-device", ...],
mdns: Espex.Mdns.MdnsLite
)

For non-Nerves setups (e.g. a host running Avahi), implement your own adapter against the Espex.Mdns behaviour — just advertise(service) and withdraw(service_id):

defmodule MyApp.AvahiAdapter do
@behaviour Espex.Mdns
@impl true
def advertise(service), do: MyApp.Avahi.publish(service)
@impl true
def withdraw(id), do: MyApp.Avahi.unpublish(id)
end
Espex.start_link(mdns: MyApp.AvahiAdapter, ...)

Development

mix deps.get
mix compile
mix test
mix credo --strict
mix dialyzer

An interactive demo that starts a server advertising a switch, button and sensor and walks you through an HA connection:

mix run test/manual/live_demo.exs # plaintext
ESPEX_ENCRYPT=1 mix run test/manual/live_demo.exs # Noise-encrypted

Roadmap

License

MIT — see LICENSE.