Behaviour for entity providers — the pluggable source of custom ESPHome entities (sensors, switches, lights, covers, climate, …) beyond the built-in serial, Z-Wave, and infrared proxies.
See the "Entity types" guide for the specific proto structs and field semantics for each entity type. This module documents the behaviour itself and the two common patterns for implementing it.
Callbacks
| Callback | When called |
|---|---|
list_entities/0 | Once per connection, when the client sends ListEntitiesRequest |
initial_states/0 | Once per connection, when the client sends SubscribeStatesRequest |
handle_command/1 | Each time the client issues a command struct for one of your entities |
Return values:
list_entities/0andinitial_states/0return lists ofEspex.Proto.*structs. Use the advertisement structs (ListEntities*Response) in the first, and the state structs (*StateResponse) in the second. See the entity-types guide.handle_command/1returns:okon success or{:error, term}on failure. Espex currently logs errors and continues — it does not send an error back to the client.
Frozen-at-accept-time snapshot
list_entities/0 is called exactly once per connection, at accept
time. The returned list is cached by the connection handler for the
lifetime of the connection. ESPHome clients (including Home
Assistant) also cache the advertisement after the first
ListEntitiesRequest.
Implication: if you dynamically add or remove entities at runtime, existing clients will continue to see the set that was returned when they connected. A reconnect is required to pick up changes.
The stateful provider pattern (GenServer)
Most real providers hold mutable state — current values of each
entity, observer pids, connections to downstream hardware — so a
GenServer is the natural fit. The provider module implements both
the Espex.EntityProvider behaviour and the GenServer behaviour;
the behaviour callbacks typically delegate reads into GenServer.call/2
and commands into GenServer.cast/2 against itself.
defmodule MyApp.Entities do
@behaviour Espex.EntityProvider
use GenServer
alias Espex.Proto
@switch_key 1001
@sensor_key 1002
# --- public API ---
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
def set_sensor(value), do: GenServer.call(__MODULE__, {:set_sensor, value})
# --- EntityProvider callbacks ---
@impl Espex.EntityProvider
def list_entities do
[
%Proto.ListEntitiesSwitchResponse{
object_id: "demo_switch", key: @switch_key, name: "Demo Switch"
},
%Proto.ListEntitiesSensorResponse{
object_id: "demo_sensor", key: @sensor_key, name: "Demo Sensor",
unit_of_measurement: "°C", accuracy_decimals: 1
}
]
end
@impl Espex.EntityProvider
def initial_states do
GenServer.call(__MODULE__, :initial_states)
end
@impl Espex.EntityProvider
def handle_command(command) do
GenServer.cast(__MODULE__, {:command, command})
end
# --- GenServer callbacks ---
@impl GenServer
def init(opts) do
{:ok, %{switch: false, sensor: 20.0, server: Keyword.fetch!(opts, :server)}}
end
@impl GenServer
def handle_call(:initial_states, _from, state) do
responses = [
%Proto.SwitchStateResponse{key: @switch_key, state: state.switch},
%Proto.SensorStateResponse{key: @sensor_key, state: state.sensor, missing_state: false}
]
{:reply, responses, state}
end
def handle_call({:set_sensor, value}, _from, state) do
Espex.push_state(state.server, %Proto.SensorStateResponse{
key: @sensor_key, state: value, missing_state: false
})
{:reply, :ok, %{state | sensor: value}}
end
@impl GenServer
def handle_cast({:command, %Proto.SwitchCommandRequest{key: @switch_key, state: s}}, state) do
Espex.push_state(state.server, %Proto.SwitchStateResponse{key: @switch_key, state: s})
{:noreply, %{state | switch: s}}
end
def handle_cast({:command, _unknown}, state), do: {:noreply, state}
endNote the two key moves:
handle_command/1casts back to self. Commands arrive on whichever connection handler received them; delegating to the provider GenServer keeps the single owner of the state the serializing point for all updates.push_state/2broadcasts to every connection. Each subscribed connection'shandle_info/2will see the state frame. If only the commanding client is connected, it's still the right behaviour — multiple HA installs or other clients stay in sync.
The stateless provider pattern
When your "state" lives outside espex (an external API, a database, an ETS table owned by another process), you don't need a GenServer. Implement the three callbacks as plain module functions:
defmodule MyApp.Readonly do
@behaviour Espex.EntityProvider
alias Espex.Proto
@impl true
def list_entities do
[
%Proto.ListEntitiesBinarySensorResponse{
object_id: "door_open", key: 1, name: "Door Open", device_class: "door"
}
]
end
@impl true
def initial_states do
[
%Proto.BinarySensorStateResponse{
key: 1, state: MyApp.DoorSensor.current(), missing_state: false
}
]
end
@impl true
def handle_command(_), do: :ok # no commands for read-only sensors
endPushing state updates
Espex.push_state/2 broadcasts a state struct to every currently
connected client that's subscribed via SubscribeStatesRequest.
Pass the server name you configured (defaults to Espex.Server):
Espex.push_state(MyApp.EspexServer, %Proto.SensorStateResponse{
key: 1002,
state: 23.5,
missing_state: false
})With no subscribed clients, the broadcast is a silent no-op. See the "Architecture" guide for the underlying Registry fan-out mechanics.
Wiring
Start the provider under your own supervisor (or the application tree), then point espex at it:
children = [
{MyApp.Entities, server: MyApp.EspexServer},
{Espex,
name: MyApp.EspexSup,
server_name: MyApp.EspexServer,
device_config: [name: "my-device"],
entity_provider: MyApp.Entities}
]
Supervisor.start_link(children, strategy: :rest_for_one)Starting the provider before espex (and using :rest_for_one)
ensures espex can't receive a connection before the provider is
ready to answer list_entities/0.
Summary
Callbacks
Handle a command from an ESPHome client — e.g.
%Espex.Proto.SwitchCommandRequest{}, %Espex.Proto.LightCommandRequest{}.
Return the initial state for each advertised entity, sent when a
client subscribes. One struct per entity — typically the matching
Espex.Proto.*StateResponse message for the entity type.
Return the list of entities this provider advertises. One struct per
entity — typically a mix of the various Espex.Proto.ListEntities*Response
messages.
Callbacks
Handle a command from an ESPHome client — e.g.
%Espex.Proto.SwitchCommandRequest{}, %Espex.Proto.LightCommandRequest{}.
@callback initial_states() :: [struct()]
Return the initial state for each advertised entity, sent when a
client subscribes. One struct per entity — typically the matching
Espex.Proto.*StateResponse message for the entity type.
@callback list_entities() :: [struct()]
Return the list of entities this provider advertises. One struct per
entity — typically a mix of the various Espex.Proto.ListEntities*Response
messages.