Matter protocol is experimental
Matter is only supported on ESP32x based devices and requires a specific build with
Below are implementation notes to understand and extend Matter.
The plugin system is designed to have different implementations for different types of devices or sensors. Each Matter endpoint is managed by an instance of a Plugin class.
root (0) is managed by the
matter.Plugin_Root class because of its specific behavior.
We provide currently the following classes:
|Plugin_Device||Generic device (abstract)|
|Plugin_Root.||Root node (type |
|Plugin_Aggregator||Aggregator for Bridge mode (type |
|Plugin_OnOff||Simple On/Off Plug (type |
|Plugin_Light0||Light with 0 channel (OnOff) (type 0x0100)|
|Plugin_Light1||Light with 1 channels (Dimmer) (type 0x0101)|
|Plugin_Light2||Light with 2 channels (CT) (type 0x010C)|
|Plugin_Light3||Light with 3 channels (RGB) (type 0x010D)|
|Plugin_Sensor||Generic Sensor class (abstract)|
|Plugin_Sensor_Temp||Temperature Sensor (type 0x0302)|
|Plugin_Sensor_Pressure||Pressure Sensor (type 0x0305)|
|Plugin_Sensor_Light||Light/Illuminance Sensor (type 0x0106)|
|Plugin_Sensor_Humidity||Humidity Sensor (type 0x0307)|
|Plugin_Sensor_Occupancy||Occupancy Sensor linked to a swithc (type 0x0107)|
Tasmota is also able to act as a Bridge to other Tasmota devices (ESP8266 or ESP32) and drive them via the HTTP API. The following classes provide such features:
|Plugin_Bridge_HTTP||Generic superclass for remote devices (abstract)|
|Plugin_Bridge_OnOff||Simple On/Off Plug (type |
|Plugin_Bridge_Light0||Light with 0 channel (OnOff) (type 0x0100)|
|Plugin_Bridge_Light1||Light with 1 channels (Dimmer) (type 0x0101)|
|Plugin_Bridge_Light2||Light with 2 channels (CT) (type 0x010C)|
|Plugin_Bridge_Light3||Light with 3 channels (RGB) (type 0x010D)|
|Plugin_Bridge_Sensor||Generic Sensor class (abstract)|
|Plugin_Bridge_Sensor_Temp||Temperature Sensor (type 0x0302)|
|Plugin_Bridge_Sensor_Pressure||Pressure Sensor (type 0x0305)|
|Plugin_Bridge_Sensor_Light||Light/Illuminance Sensor (type 0x0106)|
|Plugin_Bridge_Sensor_Humidity||Humidity Sensor (type 0x0307)|
|Plugin_Bridge_Sensor_Occupancy||Occupancy Sensor linked to a swithc (type 0x0107)|
Matter_Plugin +--- Matter_Plugin_Root +--- Matter_Plugin_Aggregator +--+ Matter_Plugin_Device +--+ Matter_Plugin_Light0 | |--+ Matter_Plugin_Light1 | |--- Matter_Plugin_Light2 | |--- Matter_Plugin_Light3 +--- Matter_Plugin_OnOff +--+ Matter_Plugin_Shutter | +--- Matter_Plugin_ShutterTilt +--+ Matter_Plugin_Sensor | +--- Matter_Plugin_Sensor_Humidity | +--- Matter_Plugin_Sensor_Temperature | +--- Matter_Plugin_Sensor_Pressure | +--- Matter_Plugin_Sensor_Illuminance | +--- Matter_Plugin_Sensor_Occupancy +--+ Matter_Plugin_Bridge_HTTP +--+ Matter_Plugin_Bridge_Light0 | +--+ Matter_Plugin_Bridge_Light1 | | +--- Matter_Plugin_Bridge_Light2 | | +--- Matter_Plugin_Bridge_Light3 | +--- Matter_Plugin_Bridge_OnOff +--+ Matter_Plugin_Bridge_Sensor | +--- Matter_Plugin_Bridge_Sensor_Humidity | +--- Matter_Plugin_Bridge_Sensor_Temperature | +--- Matter_Plugin_Bridge_Sensor_Pressure | +--- Matter_Plugin_Bridge_Sensor_Illuminance +--- Matter_Plugin_Bridge_Sensor_Occupancy
All plugins inherit from the
Note: for solidification to succeed, you need to declare
class Matter_Plugin end fake class in the same Berry file. The actual class will be used in solidified code.
|init(device, endpoint)||(can be overridden) Instantiate the plugin on a specific |
matter.Device used as monad
matter_device is a monad of
matter.Device automatically created at boot. It checks if Matter si enabled (
SetOption151 1) and instantiates all sub-systems.
|plugins||List of |
Each plugin manages a distinct endpoint and the associated sub-device behavior
|udp_server||instance of |
|message_handler||instance of |
|sessions||instance of |
All active persistent and non-persistent sessions are listed here, and serve to dispatch incoming packets
Session are also linked to
|ui||instance of |
Handles the web UI for Matter.
The following are saved as Matter device configuration
|nextep||(int) next endpoint to be allocated for bridge, start at 51|
When commissioning is open, here are the variables used:
|commissioning_open||timestamp for timeout of commissioning (millis()) or |
|commissioning_iterations||current PBKDF number of iterations|
|commissioning_w0||current w0 (SPAKE2+)|
|commissioning_L||current L (SPAKE2+)|
|commissioning_instance_wifi||random instance name for commissioning (mDNS)|
|commissioning_instance_eth||random instance name for commissioning (mDNS)|
For default commissioning, the following values are used (and can be changed via UI):
|Root Commissioning variables||Description|
|root_iterations||PBKDF number of iterations|
|PBKDF information used only during PASE (freed afterwards)|
|start_root_basic_commissioning(timeout_s)||Start Basic Commissioning with root/UI parameters |
Open window for
|remove_fabric(fabric)||Remove a fabric and clean all corresponding values and mDNS entries|
|start_basic_commissioning(timeout_s, iterations, discriminator, salt, w0, L, admin_fabric)||Start Basic Commissioning Window with custom parameters|
|is_root_commissioning_open()||Is root commissioning currently open. Mostly for UI to know if QRCode needs to be shown.|
|stop_basic_commissioning()||Stop PASE commissioning, mostly called when CASE is about to start|
|compute_qrcode_content()||Compute QR Code content - can be done only for root PASE|
|compute_manual_pairing_code()||Compute the 11 digits manual pairing code (without vendorid nor productid) p.223 |
can be done only for root PASE (we need the passcode, but we don't get it with OpenCommissioningWindow command)
|every_second()||Dispatch second-resolution ticks to: sessions, message_handler, plugins. |
Expire commissioning window.
Called by Tasmota loop.
|start_operational_discovery_deferred(session)||Start Operational Discovery for this session |
Deferred until next tick.
|start_commissioning_complete_deferred(session)||Start Commissioning Complete for this session |
Deferred until next tick.
|start_operational_discovery(session)||Start Operational Discovery for this session |
Stop Basic Commissioning and clean PASE specific values (to save memory). Announce fabric entry in mDNS.
|start_commissioning_complete(session)||Commissioning Complete |
Stop basic commissioning.
|get_active_endpoints(exclude_zero)||Return the list of endpoints from all plugins (distinct), exclude endpoint zero if |
|save_param()||Persistence of Matter Device parameters|
|load_param()||Load Matter Device parameters|
Incoming messages handing
|msg_received(raw, addr, port)||Callback when message is received. |
|msg_send(raw, addr, port, id)||Global entry point for sending a message. |
|received_ack(id)||Signals that a ack was received. |
|attribute_updated(endpoint, cluster, attribute, fabric_specific)||Signal that an attribute has been changed and propagate to any active subscription. |
|process_attribute_expansion(ctx, cb)||Proceed to attribute expansion (used for Attribute Read/Write/Subscribe) |
Called only when expansion is needed, so we don't need to report any error since they are ignored
In case of
|invoke_request(session, val, ctx)||Matter plugin management |
Plugins allow to specify response to read/write attributes and command invokes
|init(raw, addr, port, id)||Create raw UDP packet with |
|send(socket)||Send packet now. Returns |
|start_mdns_announce_hostnames()||Start mDNS and announce hostnames for Wi-Fi and ETH from MAC |
When the announce is active,
|mdns_announce_PASE()||Announce MDNS for PASE commissioning|
|mdns_remove_PASE()||MDNS remove any PASE announce|
|mdns_announce_op_discovery_all_fabrics()||Start UDP mDNS announcements for commissioning for all persisted sessions|
|mdns_announce_op_discovery(fabric)||Start UDP mDNS announcements for commissioning|
|mdns_remove_op_discovery_all_fabrics()||Remove all mDNS announces for all fabrics|
|mdns_remove_op_discovery(fabric)||Remove mDNS announce for fabric|
|save_before_restart()||Try to clean MDNS entries before restart. |
Called by Tasmota loop as a Tasmota driver.
This class creates a monad (singleton) in charge of receiving and sending all UDP packets. Packets to sent are generally put in a queue, and are re-sent with exponential backoff until they are acknowledged by the receiver (as part of Matter over UDP) or after the maximum of retries have been made.
|init(addr, port)||Init UDP Server listening to |
|start(cb)||Starts the server. Registers as device handler to Tasmota. |
Raises an exception if something is wrong.
|stop()||Stops the server and remove driver|
|every_50ms()||At every tick: Check if a packet has arrived, and dispatch to |
Then resend queued outgoing packets.
|_resend_packets()||Resend packets if they have not been acknowledged by receiver either with direct Ack packet or ack embedded in another packet. Packets with |
Packets are re-sent at most
If all retries expired, remove packet and log.
|received_ack()||Just received acknowledgment, remove packet from sender|
|send_response(raw, addr, port, id, session_id)||Send a packet, enqueue it if |
matter_device.message_handler is a monad of
Dispatches incoming messages and sends outgoing messages
|Variables of Message Handler||Description|
|device||Reference to the global |
|commissioning||Commissioning Context instance, handling the PASE/CASE phases|
|im||Instance of |
|init(device)||Constructor, instantiates monads for |
|msg_received(raw, addr, port)||Called by |
- decodes the message header
- associates the message with the corresponding active session, or create a new session
- dispatches to
- sends an Ack packet if the received packet had the
|send_response(raw, addr, port, id, session_id)||Send a packet. Proxy to the same method in |
Implements the TLV encoding and decoding as defined in Appendix A of the Matter specification. TLV stands for Tag-length-value encoding. It is a way to encode tagged values and structures in a binary compact format. Most Matter messages are encoded in TLV.
Parse and print:
m = matter.TLV.parse(b) print(m)
|I1 I2 I4||Signed integer of at most (1/2/4) bytes (as 32 bits signed Berry type)|
|U1 U2 U4||Unsigned integer of at most (1/2/4) bytes (as 32 bits signed Berry type, be careful when comparing. Use |
|I8 U8||Signed/unsigned 8 bytes. You can pass |
|BOOL||Boolean, takes |
|FLOAT||32 bites float|
|UTF1 UTF2||String as UTF, size is encoded as 1 or 2 bytes automatically|
|B1 B2||raw |
|NULL||takes only |
|(internal) Use through abstractions|
|Unsupported in Tasmota|
matter.TLV.create_TLV(matter.TLV.UTF1, "Hello world")
When a subscription is issued by an initiator, we create an instance of
matter.IM_Subscription which holds:
CASE sessionon which the subscription was issued. If the session is closed, the subscription dies. Subscriptions are not persisted and stop if reboot
subscription_id(int) used to tell the initiator which subscription it was
matter.Pathinstances recording all the attributes subscribed to. They can include wildcards
max_interval(in seconds): Tasmota waits at least
min_intervalbefore sending a new value, and sends a message before
max_interval(usually heartbeats to signal that the subscription is still alive). Generally changes to attributes are dispatched immediately.
fabric_filtered: not used for now
Below are internal arguments:
not_before: the actual timestamp that we should wait before sending updates, as to respect
expiration: the maximum timestamp we can wait before sending a heartbeat. Both are updated after we sent a new value
wait_status: signals that we sent everything and we wait for the final
StatusReportto resume sending further updates
is_keep_alive(bool) did the last message was a keep-alive, if so we just expect a Ack and no StatusReport
updates: list of concrete attributes that have values changed since last update. They don't contain the new value, we will actually probe each attribute value when sending the update
This class (monad) contains the global list of all active subscriptions. Method|Description :----|:--- init(im)|Instantiate the monad with the global IM monad new_subscription(session, req)|Take a session and a
SubscribeRequestMessage, parse the message and create a
matter.IM_Subscription() instance. Also allocates a new subscription id.
What happens when an attribute is updated~
Subscriptions are triggered by the value of an attribute changing. They can originate from an explicit WRITE Matter command from an initiator or another device, of be the consequence of a Matter command (like switching a light ON). The can also originated from independent source, like an action at Tasmota level (using Tasmota
Power command), or Tasmota detecting that a sensor value has changed after periodical polling.
Note: default plugins for Lights actually probe Tasmota light status every second, and report any difference between the last known change (also called shadow value) and the current status. We realized that it was more consistent and reliable than trying to create rules for every event.
When an attribute's value changed, you need to call the plugin's method
self.attribute_updated(<endpoint_id>, <cluster_id>, <attribute_id> [, <fabric_specific>])
<fabric_specific> (bool) is optional and currently ignored and reserved for future use.
endpoint_id argument is optional. If the endpoint is unknown, the message is broadcast to all endpoints that support reading this attributes:
self.attribute_updated(nil, <cluster_id>, <attribute_id>)
More generally, you can use the global method to signal an attribute change from any code:
matter_device.attribute_updated(nil, <cluster_id>, <attribute_id>)
Note: internally this method creates a
matter.Path instance and calls
which in turns calls
attribute_updated_ctx(ctx, fabric_specific) on every active subscription.
attribute_updated_ctx() are first check whether the attribute matches the filtering criteria's (that may include wildcards). If they match, the attribute is candidate to be added to the list. We then call
_add_attribute_unique_path() to check if the attribute is not already in the list, and if not add it to the list of future updates. It's possible that during the
min_interval time, an attribute may change value multiple times; yet we publish a single value (the last one).
Subscription_Shop monad checks every 250ms if there are updates ready to be sent via
It does a first scan across all active subscriptions if updates can be sent out:
- subscription is not in
wait_status(i.e. not waiting for a previous exchange to complete)
- subscription has a non-empty list of updates
- subscription has reached the
not_beforetimestamp (so as to not sent too frequent updates)
- the subscription list of updates is cleared via
Once all updates are sent, the subscription are scanned again to see if any heartbeat needs to be sent:
- subscription is not in
- subscription has reached
expirationtimestamp If so:
- the subcription list of updates is cleared via
All Matter support code is located in
berry_matter as a lib, which avoids polluting the main directory of drivers. Berry allows to develop much faster compared to C++, and performance is mostly not an issue with Matter.
The Berry code is located in the
embedded directory. Then the code is compiled into bytecode and the bytecode is stored in Flash. This avoids consuming RAM which is a very previous resource on ESP32. To solidify, you just need to run
berry_matter. But before you need to have a local version of Berry: in
berry directory, just do
make. For windows users, compiling Berry can be challenging so a pre-compiled
berry.exe is provided.