# MatterEx
A [Matter](https://csa-iot.org/all-solutions/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](https://github.com/project-chip/connectedhomeip/tree/master/examples/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 `:crypto` and `: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
```elixir
# 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
```
```elixir
# 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:
```elixir
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:
```elixir
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
end
```
For a temperature sensor, first define the device:
```elixir
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
end
```
Then push readings from your hardware:
```elixir
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
end
```
The Device API for reading and writing from Elixir:
```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:
```bash
mix test
```
chip-tool integration tests (requires `chip-tool` in PATH):
```bash
mix run test_chip_tool.exs
```
The 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:
```bash
mix run test_chip_tool.exs -- --retest
```
## Requirements
- Elixir ~> 1.17
- Erlang/OTP 26+
- No external dependencies
## License
Apache License 2.0 — see [LICENSE](LICENSE).