MatterEx

CIHex.pmDocs

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

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.

Automated Smoke Testing

Use scripts/matter_smoke.exs for a fast chip-tool based check that commissioning, CASE, OnOff commands, and BasicInformation reads still work.

# Start an in-process MatterEx device and test it with chip-tool
mix run scripts/matter_smoke.exs
# Test an already-running device, for example examples/net_test on a Raspberry Pi
mix run scripts/matter_smoke.exs -- --mode remote --host 192.168.1.42

The remote mode uses chip-tool pairing already-discovered, so it does not depend on mDNS discovery working from the test machine. Override defaults with flags such as --port 5540, --node-id 111, --passcode 20202021, or --storage-directory /tmp/matter_ex_pi4_kvs.

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
end

For 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
end

Then 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
end

The 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, ...)

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.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:

mix run test_chip_tool.exs -- --retest

Requirements

License

Apache License 2.0 — see LICENSE.