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

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