MatterEx
A Matter smart home protocol stack written in pure Elixir.
MatterEx implements the Matter application protocol from the ground up — TLV encoding, secure sessions (PASE and CASE), the Interaction Model, mDNS discovery, and 60 clusters — with zero external dependencies beyond OTP. It interoperates with chip-tool, the Matter reference controller, across 28 end-to-end integration tests covering commissioning, read/write/invoke, subscriptions, and wildcard reads.
Status: Experimental. The protocol core works and passes chip-tool interop, but this is not yet production-ready. APIs may change.
Features
- Pure Elixir — no C/C++ dependencies; all protocol logic in Elixir
- Zero external deps — only OTP's
:cryptoand:public_key - chip-tool interop — commission, establish CASE sessions, read/write attributes, invoke commands, subscribe
- Pure functional core — PASE, CASE, and MessageHandler are stateless; GenServers are thin wrappers
- 1000+ unit tests and 28 chip-tool integration tests
- 60 cluster implementations covering lighting, HVAC, sensors, locks, media, and more
Quick Start
# Define a device
defmodule MyApp.Light do
use MatterEx.Device,
vendor_name: "Acme",
product_name: "Smart Light",
vendor_id: 0xFFF1,
product_id: 0x8001
endpoint 1, device_type: 0x0100 do
cluster MatterEx.Cluster.OnOff
cluster MatterEx.Cluster.LevelControl
end
end# Start a Matter node
{:ok, _} = MyApp.Light.start_link()
MatterEx.Node.start_link(
device: MyApp.Light,
port: 5540,
passcode: 20202021,
discriminator: 3840
)The node will advertise via mDNS and accept commissioning from any Matter controller.
Endpoint 0 is auto-generated with Descriptor, BasicInformation, GeneralCommissioning, OperationalCredentials, AccessControl, NetworkCommissioning, and GroupKeyManagement.
Handling Incoming Commands
When a Matter controller (phone app, Alexa, Home Assistant, etc.) sends a command to
your device, the cluster's handle_command/3 callback is invoked. This is where you
bridge Matter to your actual hardware or application logic:
defmodule MyApp.Cluster.OnOff do
use MatterEx.Cluster, id: 0x0006, name: :on_off
attribute 0x0000, :on_off, :boolean, default: false, writable: true
attribute 0xFFFD, :cluster_revision, :uint16, default: 4
command 0x00, :off, []
command 0x01, :on, []
command 0x02, :toggle, []
@impl MatterEx.Cluster
def handle_command(:on, _params, state) do
# Control your hardware here
MyApp.GPIO.set_pin(17, :high)
{:ok, nil, set_attribute(state, :on_off, true)}
end
def handle_command(:off, _params, state) do
MyApp.GPIO.set_pin(17, :low)
{:ok, nil, set_attribute(state, :on_off, false)}
end
def handle_command(:toggle, _params, state) do
new_value = !get_attribute(state, :on_off)
if new_value, do: MyApp.GPIO.set_pin(17, :high), else: MyApp.GPIO.set_pin(17, :low)
{:ok, nil, set_attribute(state, :on_off, new_value)}
end
end
Writable attributes (like node_label) can also be changed directly by controllers
via Matter write requests — the cluster GenServer handles this automatically.
Updating State from Your Application
When something changes on your device (a button press, a sensor reading), update the Matter attribute so controllers see the new state.
Using the MyApp.Light from the Quick Start example above, here's a GenServer that
watches a GPIO button and pushes state into Matter:
defmodule MyApp.ButtonWatcher do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
def init(_opts) do
:timer.send_interval(100, :check_button)
{:ok, %{last_state: false}}
end
def handle_info(:check_button, state) do
pressed = MyApp.GPIO.read_pin(4) == :high
if pressed != state.last_state do
# Update OnOff on endpoint 1 — any subscribed controller gets notified
MyApp.Light.write_attribute(1, :on_off, :on_off, pressed)
end
{:noreply, %{state | last_state: pressed}}
end
endFor a temperature sensor, first define the device:
defmodule MyApp.Sensor do
use MatterEx.Device,
vendor_name: "Acme",
product_name: "Temp Sensor",
vendor_id: 0xFFF1,
product_id: 0x8002
endpoint 1, device_type: 0x0302 do
cluster MatterEx.Cluster.TemperatureMeasurement
end
endThen push readings from your hardware:
defmodule MyApp.TempPoller do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
def init(_opts) do
:timer.send_interval(5_000, :read_sensor)
{:ok, %{}}
end
def handle_info(:read_sensor, state) do
# Matter temperatures are in 0.01 C units (e.g., 2350 = 23.50 C)
temp = MyApp.I2C.read_temperature() |> round()
MyApp.Sensor.write_attribute(1, :temperature_measurement, :measured_value, temp)
{:noreply, state}
end
endThe Device API for reading and writing from Elixir:
MyApp.Light.read_attribute(1, :on_off, :on_off) # {:ok, true}
MyApp.Light.write_attribute(1, :on_off, :on_off, false) # :ok
MyApp.Light.invoke_command(1, :on_off, :toggle) # {:ok, nil}Architecture
UDP / TCP
|
Node (GenServer)
|
MessageHandler (pure)
/ \
PASE (SPAKE2+) CASE (Sigma)
\ /
ExchangeManager (MRP)
|
IM Router (pure)
|
Cluster GenServers
(OnOff, Thermostat, DoorLock, ...)- Node — binds UDP/TCP sockets, dispatches raw bytes
- MessageHandler — pure functional message orchestration; decrypts, routes to PASE/CASE/IM
- PASE — SPAKE2+ commissioning (passcode-based)
- CASE — certificate-authenticated session establishment (Sigma protocol)
- ExchangeManager — MRP reliability, retransmission, exchange tracking
- IM Router — dispatches Interaction Model operations to cluster GenServers
- Clusters — GenServers holding attribute state, handling commands
Clusters
60 clusters organized by function:
Lighting & Control — OnOff, LevelControl, ColorControl, FanControl, WindowCovering, PumpConfigurationAndControl
Smart Home — DoorLock, Thermostat, Switch, ModeSelect, ValveConfigurationAndControl
Sensors — TemperatureMeasurement, IlluminanceMeasurement, RelativeHumidityMeasurement, PressureMeasurement, FlowMeasurement, OccupancySensing, ElectricalMeasurement
Air Quality — AirQuality, ConcentrationMeasurement (CO2, PM2.5, PM10, TVOC), SmokeCOAlarm
Infrastructure — Descriptor, BasicInformation, AccessControl, Binding, Groups, Scenes, Identify, GeneralCommissioning, OperationalCredentials, NetworkCommissioning, GroupKeyManagement, AdminCommissioning, PowerSource, BooleanState, BooleanStateConfiguration
Diagnostics — GeneralDiagnostics, SoftwareDiagnostics, WiFiNetworkDiagnostics, EthernetNetworkDiagnostics
Localization & Time — LocalizationConfiguration, TimeFormatLocalization, UnitLocalization, TimeSynchronization
Labels — FixedLabel, UserLabel
OTA — OTASoftwareUpdateProvider, OTASoftwareUpdateRequestor
Energy — DeviceEnergyManagement, EnergyPreference, PowerTopology
Media — MediaPlayback, ContentLauncher, AudioOutput
Appliances — LaundryWasherControls, DishwasherAlarm, RefrigeratorAlarm
ICD — ICDManagement
Testing
Unit tests:
mix test
chip-tool integration tests (requires chip-tool in PATH):
mix run test_chip_tool.exsThe integration test commissions a device, then runs 28 steps: OnOff toggle/on/off, BasicInformation reads, Descriptor validation, ACL reads, Identify invoke, Groups, Scenes, timed interactions, wildcard reads, error paths, and subscriptions.
Re-run only previously failed tests:
mix run test_chip_tool.exs -- --retestRequirements
- Elixir ~> 1.17
- Erlang/OTP 26+
- No external dependencies
License
Apache License 2.0 — see LICENSE.