ESPHome Native API server library.
Espex implements the ESPHome Native API protocol over TCP, letting an Elixir application expose itself as an ESPHome device to clients such as Home Assistant. The wire protocol, connection lifecycle, and optional Noise-encrypted transport all live here; hardware is plugged in through behaviours.
Documentation map
- Architecture — supervision tree, the
connection/dispatch split, wire protocol, encryption, and the
push_state/2broadcast path - Entity types — per-type cookbook for the common ESPHome entities (Switch, BinarySensor, Sensor, Button, Light, Cover, Climate) with proto structs and example snippets
Espex.SerialProxy,Espex.ZWaveProxy,Espex.InfraredProxy,Espex.BluetoothScanner,Espex.BluetoothProxy,Espex.EntityProvider,Espex.Mdns— the seven behaviours, each with callback reference and a complete example adapter
Quick start
Start under your own supervision tree:
children = [
{Espex,
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}
]
Supervisor.start_link(children, strategy: :one_for_one)Any adapter key you omit disables that feature. For encrypted
transport, set :psk on :device_config:
device_config: [
name: "my-device",
psk: "foIclFXDcBlfzi9oQNegJz/uRG/sgdIc956pX+GrC+A="
]For the full start option list see Espex.Supervisor.
Pushing state
Call push_state/2 from anywhere in your application to broadcast
an entity state update to every currently-connected client:
Espex.push_state(%Espex.Proto.SensorStateResponse{
key: 1003,
state: 21.3,
missing_state: false
})Connected clients
Enumerate the live native-API connections with connected_clients/1,
and subscribe to changes by configuring an Espex.ConnectionListener:
Espex.connected_clients(MyApp.EspexServer)
#=> [%Espex.ClientInfo{peer: "192.168.1.5:54312", encrypted?: true, ...}]
Summary
Functions
child_spec/1 — makes {Espex, opts} usable as a child spec.
List the currently-connected native-API clients as
Espex.ClientInfo structs.
Return the running server's %DeviceConfig{}. Accepts an optional
server name for non-default supervisor setups.
Broadcast an entity-state struct to every currently-connected client.
Start the full Espex supervision tree with the given options.
Functions
@spec child_spec(keyword()) :: Supervisor.child_spec()
child_spec/1 — makes {Espex, opts} usable as a child spec.
@spec connected_clients(atom()) :: [Espex.ClientInfo.t()]
List the currently-connected native-API clients as
Espex.ClientInfo structs.
This is a non-blocking read of the connection Registry — the source of
truth for the live set. Pass a custom :server_name if you started the
supervisor with one; it defaults to Espex.Server.
Entries appear from TCP accept (so a client that hasn't finished its
HelloRequest yet shows client_info: nil / api_version: nil) and
vanish when the connection closes. To be told when the set changes
without polling, configure an Espex.ConnectionListener.
Espex.connected_clients(MyApp.EspexServer)
#=> [%Espex.ClientInfo{client_info: "Home Assistant 2026.1.0", ...}]
@spec device_config(GenServer.server()) :: Espex.DeviceConfig.t()
Return the running server's %DeviceConfig{}. Accepts an optional
server name for non-default supervisor setups.
Broadcast an entity-state struct to every currently-connected client.
Pass any %Espex.Proto.*StateResponse{} (e.g.
%Espex.Proto.SensorStateResponse{key: k, state: 21.3}). Clients that
subscribed via SubscribeStatesRequest will receive the frame over
their socket.
server_name defaults to Espex.Server — pass your custom name if
you started the supervisor with :server_name.
@spec start_link(keyword()) :: Supervisor.on_start()
Start the full Espex supervision tree with the given options.