Skip to content

About

Open-source Matter over Thread firmware for Shelly 1 Gen4, 1 Mini Gen4, 1PM Gen4 and 2PM Gen4 (ESP32-C6). One image for all four, Lua-scriptable endpoints, power metering, Add-on sensors, Matter OTA. No cloud, no WiFi needed. Home Assistant, Apple Home, Google Home.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

Shelly Gen4 Matter Module — open-source Matter over Thread firmware for Shelly 1 Gen4, 1 Mini Gen4, 1PM Gen4 and 2PM Gen4

Latest release License: MIT Matter 1.6 Thread ESP32-C6

Replacement firmware that turns a Shelly 1 Gen4, Shelly 1 Mini Gen4, Shelly 1PM Gen4 or Shelly 2PM Gen4 (all ESP32-C6) into a native Matter over Thread device: no Shelly app, no cloud account, no WiFi required. Works with Home Assistant, Apple Home, Google Home and any other Matter 1.6 controller with a Thread border router.

One firmware image for all four models — pick the model on the built-in management page, no separate downloads.

Highlights

  • Matter over Thread, BLE commissioning — behaves like an off-the-shelf Matter device: scan the code, done. Thread is the only network it needs.
  • Install from the stock Shelly web UI — no UART, no opening the device. Return to the official Shelly firmware from the same page.
  • Configurable Matter endpoints without reflashing — light switch (on/off, dim, colour temperature via Matter bindings), relay, temperature, occupancy, contact and electrical power measurement endpoints, up to 8 per module, chosen at runtime.
  • Lua 5.4 scripting — per-endpoint button/relay/sensor logic, editable in the browser, stored on the module.
  • Power metering — BL0942 (1PM) and dual-channel ADE7953 (2PM) exposed as Matter Electrical Power Measurement endpoints.
  • Shelly Plus Add-on support — turn a relay module into a multi-sensor: DS18B20 temperature, a touch sensor (TTP223) or presence radar (HLK-LD2410S) on the digital input, and a 0–10 V analog input, each as its own Matter endpoint. See Shelly Plus Add-on.
  • Matter bindings, unicast and group (multicast) — control lights directly from the wall switch, controller-independent.
  • Management dashboard reachable over Thread (IPv6) — scripts, hardware, diagnostics, log, backup/restore. WiFi can be switched on for 10 minutes or permanently next to Thread when you prefer it.
  • Updates — Matter OTA (e.g. from Home Assistant), .bin upload on the dashboard, or the Shelly-style .zip package; A/B partitions with rollback.
  • Fabric recovery — removing the device from one controller reopens the commissioning window; removing the last one reboots into commissioning.

Version history: see CHANGELOG.md.

Disclaimer

⚠️ Read this before you flash anything.

Installing this firmware voids your Shelly warranty, and Shelly cannot provide technical support for a device running third-party code. It can remove the factory keys that enable Shelly Cloud and official OTA updates. Treat flashing as one-way unless you keep the full-chip backup you make before flashing. Incorrect flashing can brick your device, so always back up your original firmware before proceeding if reversibility is important to you. You assume all responsibility for any damage, data loss, or device failure.

This project is not affiliated with Shelly or Espressif Systems.

Endpoint types

Each of the 8 script slots can be configured as one of these Matter endpoint types from the management dashboard, without reflashing or re-commissioning:

Type Matter device Description
OnOff Toggle + Dim + Color 0x0103 Light Switch (client) Toggle, dim, color temp via bindings
OnOff State-follow 0x0103 Light Switch (client) On/Off follows switch position
Relay (OnOff Light) 0x0100 OnOff Light (server) Physical relay (GPIO from the active hardware profile; 2 relays on the 2PM)
Temperature Sensor 0x0302 Temp. Sensor (server) DS18B20 via 1-Wire, or any value set from Lua
Occupancy Sensor 0x0107 Occupancy Sensor (server) Analog IN duty cycle, or set from Lua
Illuminance Sensor 0x0106 Light Sensor (server) Lux value set from Lua
Contact Sensor 0x0015 Contact Sensor (server) Boolean state set from Lua
Electrical Power Measurement 0x0510 Electrical Sensor (server) Voltage/current/power/energy from the BL0942 (1PM) or ADE7953 (2PM), or set from Lua

Based on

  • esp-matter release/v1.6 (Matter 1.6)
  • connectedhomeip (as submodule within esp-matter)
  • ESP-IDF v5.5.5
  • Lua 5.4 (compiled as component)

Target hardware

One firmware image supports four Gen4 models (all ESP32-C6, 8 MB flash). Select the model on the management dashboard (Hardware → Device Type); the choice is stored in NVS and applied on the next boot. The correct GPIO mapping is then used for the relay(s), wall-switch input(s), onboard button and status LED.

Model Relay Switch Button Status LED Add-on (Digital IN) Power meter Board NTC
Shelly 1 Gen4 (default) GPIO5 GPIO10 GPIO4 GPIO15 yes (GPIO18) — —
Shelly 1 Mini Gen4 GPIO10 GPIO12 GPIO22 GPIO5 — — GPIO4
Shelly 1PM Gen4 GPIO4 GPIO10 GPIO1 GPIO11 yes (GPIO12) BL0942 (UART1 GPIO7 + GPIO6, 9600 baud) GPIO3
Shelly 2PM Gen4 GPIO5 + GPIO3 GPIO11 + GPIO10 GPIO12 GPIO18 yes (GPIO1) ADE7953 dual-channel (IRQ=GPIO19, I2C SDA=GPIO6 SCL=GPIO7) GPIO4

See STOCK_GPIO.md for the evidence per field. The Add-on Analog IN (GPIO17) and 1-Wire (GPIO16 in / GPIO9 out) are identical on every model; only Digital IN moves.

The polarity is the same on every model: relays are active-high, status LEDs active-low, the onboard button and Add-on Digital IN active-low (internal pull-up), and the wall-switch inputs active-high in normal operation (see BENCH_MODE for the development exception).

ℹ️ Shelly 1 Mini Gen4 is supported. Since installing straight from the stock Shelly web UI now works (see Firmware updates), the Mini can be flashed without opening it. The Mini has no Shelly Plus Add-on connector, so the add-on inputs are unavailable on it.

⚠️ Test status: only the Shelly 1 Gen4 and Shelly 1 Gen4 mini has been verified on real hardware. The other profiles match the stock firmware pin-for-pin but have not been hardware-tested. The BL0942 and ADE7953 scaling constants are placeholders and must be calibrated against a known load on real hardware before the reported voltage/current/power values are trustworthy.

⚠️ ADE7953 I2C pins are the one unverified value. Stock reads the I2C SDA/SCL pins from the device configuration in NVS instead of hardcoding them. Only the ADE7953 IRQ (GPIO19) is confirmed. The 2PM relay/switch pins themselves are confirmed against the stock firmware (relays on GPIO5/GPIO3, switches on GPIO11/GPIO10).

Notes:

  • Changing the device type does not require Matter re-commissioning — the firmware exposes a generic switch model, so a type change is only a GPIO remap.
  • Warning: selecting the wrong model drives the wrong GPIOs. Pick the model that matches your physical hardware.
  • The Shelly Plus Add-on (DS18B20 + touch + analog occupancy) is available on the 1 Gen4, 1PM Gen4 and 2PM Gen4.
  • On the 1PM Gen4 the BL0942 reports voltage, current, active power, accumulated energy and line frequency via a Matter Electrical Power Measurement endpoint, and on the dashboard Hardware tab.
  • The 2PM Gen4 uses an ADE7953 measuring two channels (A = relay 1, B = relay 2). Each channel is exposed as its own Electrical Power Measurement endpoint. The two relays are two OnOff Light endpoints and both wall-switch inputs are reported to scripts (see the Lua section).
  • The 1 Mini, 1PM and 2PM Gen4 have an onboard NTC (10 kΩ divider, B ≈ 3350), read every 10 s and shown on the Hardware tab as Board temperature (NTC) and in Lua as input.board_temperature(). Above 95 °C on two consecutive readings while a relay is on, every relay switches off (overheat protection). The 1 Gen4 has no NTC; its Chip temperature is the ESP32-C6 die sensor.
Component Details
Shelly Plus Add-on DS18B20 (TX=GPIO9/RX=GPIO16) + Digital IN (GPIO18 on the 1 Gen4) + Analog IN (GPIO17)
Thread Border Router Google TV Streamer 4K (or any Thread BR)
Matter controller Home Assistant Matter Server, Google Home, Apple Home
Commissioning HA Matter Server UI or chip-tool

Shelly Plus Add-on

The official Shelly Plus Add-on clips onto the 1 Gen4, 1PM Gen4 and 2PM Gen4 (not the Mini) and gives the module galvanically isolated low-voltage inputs behind the mains-side relay. With this firmware every Add-on input can be wired to its own Matter endpoint from a Lua script, so one module in a wall box becomes a relay plus a sensor — no extra Thread device, no extra power supply, and the sensor logic (e.g. "presence → light on, no presence for 5 min → light off") runs locally on the module, even when the controller is down.

Add-on terminal Typical sensor Matter endpoint Lua
Digital IN TTP223 capacitive touch pad (a touch surface instead of a wall switch), reed/door contact, PIR or radar with a digital OUT such as the HLK-LD2410S Contact Sensor, Occupancy Sensor, or a button that toggles/controls a bound light input.digital(), contact_closed/contact_open and all button events with input.button_id() == 1
Analog IN (0–10 V) Presence sensors with an analog/PWM output, light-dependent resistors, 0–10 V process signals Occupancy Sensor, Illuminance Sensor input.analog()
DS18B20 (1-Wire) Temperature probe, up to 1 sensor on the bus Temperature Sensor input.temperature()

⚠️ Power budget: the Add-on's sensor supply delivers at most 10 mA. Only connect sensors that stay below that: a TTP223 (a few µA), a DS18B20 (≈1 mA while converting), a reed contact or the HLK-LD2410S are fine. The LD2410S is the special low-power variant and the one to buy for this — the regular HLK-LD2410/LD2410B/LD2410C draw far more than 10 mA and cannot be powered from the Add-on. Anything that needs more than 10 mA (mmWave radars other than the S variant, PIR boards with a relay output, modules with indicator LEDs) must not be connected to the Add-on supply at all.

Digital IN has a built-in pull-up: the input idles high and reads true when the sensor pulls it to GND (< 0.5 V; > 2.5 V = idle). The sensor's output must therefore be able to sink the pin — open-drain/open-collector, a switch or reed contact to GND, or a push-pull output that swings between GND and its supply. An output that is idle-high and goes high on detection (TTP223 in its default active-high mode, the LD2410S OUT pin) works, but reads inverted: either select the active-low option of the sensor (TTP223 AHLB pad) or invert the value in your Lua script.

The wiring and polarity of each Add-on GPIO are listed under Pin mapping; example scripts for a touch pad, a presence sensor and a temperature probe are in SCRIPTS.md.

Setup procedure

1. First flash

Keep a full backup before flashing (see the warning under Firmware updates).

There are two ways to install the firmware for the first time.

Option A — from the stock Shelly web UI (no UART, no opening the device):

idf.py build
python3 tools/make-webui-ota-zip.py     # → shelly-gen4-matter-module-<model>-v<version>-ota.zip, one per model
python3 tools/make-webui-ota-zip.py --model 1-mini-gen4   # only one model

The firmware inside is the same for every model; only the manifest differs. The stock updater installs an unsigned package only when its manifest name is the module's own Shelly app code (S1G4, Mini1G4, S1PMG4, S2PMG4). A package for another model fails with Signature verification of manifest for S1G4 failed. Verified on a Shelly 1 Gen4 running stock 2.0.1. After the install, select the model under Hardware → Device Type on the management dashboard.

  • Upload it through the stock Shelly device page: (Settings → Firmware, "install from file"):

Background info about the webui ota package:

The package ships no bootloader — only nvs, app and fs. It keeps the stock "Shelly OS loader" so the stock updater's own A/B flow reliably boots our app in the inactive slot. On the first boot the firmware then performs a one-time self-migration: it writes our ESP-IDF bootloader to 0x0 plus valid otadata and reboots. From then on the module boots via the standard ESP-IDF bootloader + otadata, so OTA no longer depends on the stock loader's proprietary SH0S boot-select or on future Shelly loader changes. This is the only way to install on a Shelly 1 Mini Gen4, which has no accessible UART pads.

Option B — UART flash (see INSTALL.md): open the device and wire a USB-UART adapter to the J6 connector. This installs the ESP-IDF bootloader together with the partition table and app, and is also the way to make a full 8 MB backup of the stock Shelly firmware for a guaranteed return to stock.

After either first install, all further updates are over the air (Matter OTA / dashboard .bin upload, or a fresh web-UI package) — no UART needed.

2. Factory reset → commissioning mode

Home Assistant prerequisite (once): this firmware uses the Matter test vendor ID (0xFFF1) and a test Device Attestation Certificate, so the Matter Server has to accept test-net devices. In Settings → Add-ons → Matter Server → Configuration turn on "Enable test-net DCL usage" and restart the add-on. Without it commissioning fails at the attestation step. The same switch is what later lets Home Assistant offer Matter OTA updates for this firmware, so enabling it now covers both.

After flashing (or a factory reset via the web interface) the module comes up in BLE commissioning mode straight away — no WiFi, no setup step first:

  1. Open Home Assistant companion App → Settings → Devices & Services → Matter → "Add device"
  2. Enter setup code: 34970112332 (default, configurable in sdkconfig.defaults)
  3. HA Matter Server pairs via BLE and provisions Thread credentials
  4. After ~30-60s the device appears in HA

A module that is already commissioned skips this and goes straight to Thread. Removing the device from its last controller (e.g. deleting it in Home Assistant) reboots it into this commissioning mode a few seconds later; WiFi credentials and scripts are kept. If another fabric is still present (typical when HA was commissioned from the Android app, which leaves a Google fabric next to HA's — see Fabrics in /api/diag), the module instead opens its commissioning window for 15 minutes over the network, so it can be re-added with the same QR/pairing code through the Thread border router without touching the other fabrics.

3. Configure endpoints via the dashboard

Endpoints do not have to exist before commissioning. Open the management dashboard (over Thread, or via the 10-minute WiFi window — see below: Reach the management dashboard over Thread) and go to the Scripts tab:

  1. Set a name, endpoint type, trigger, and Lua script for each slot you need
  2. Click Save for each slot
  3. Click Reboot on the Scripts page

The new endpoints appear in Home Assistant by themselves; if not, re-interview the device (Matter integration → device → Reconfigure/interview).

See SCRIPTS.md for example scripts.

4. Normal operation

After commissioning, Thread + Bluetooth are active for Matter communication. WiFi is off by default.

Reach the management dashboard over Thread

The dashboard is served over IPv6 on the Thread network, so you can reach it without WiFi: Easiest way to find the IPv6 addres of the module is in Home Assistant → Devices → Matter → Click on device → Click on Matter info. Use this IPv6 address in the browser between square brackets, example: http://[ipv6 adress of module]

Other ways to find the IPv6 address:

  • Direct (always works): the module logs its addresses at boot. Use the OMR / SLAAC address (marked <-- OMR in the log, e.g. fd96:…) and open http://[<omr-ipv6>]/ from any host that routes to the Thread network through your border router. The mesh-local (fd…:0:0:ff:fe00:…) and link-local (fe80:…) addresses are not routable off-mesh.
  • By name (mDNS): the module advertises an _http._tcp service (instance label = configured hostname) via the Matter SRP client. A border router with an advertising proxy re-publishes it as LAN mDNS, so you can discover it with avahi-browse -rt _http._tcp / dns-sd -B _http._tcp, or generate a clickable overview of all modules with tools/shelly-overview.sh. Note the resolvable .local name is the opaque CHIP SRP host (e.g. 52E2….local), not the friendly hostname — the friendly hostname is the service label you browse.

SRP fallback server (discovery without a border router) background info

Direct device-to-device control (a switch bound to a lamp) needs the switch to resolve the lamp's operational address over Thread DNS-SD. That resolution is answered by the Thread network's SRP / DNS-SD server. Some border routers register services and proxy them to LAN mDNS, but do not answer the operational-discovery query a Thread node makes for another node — so a freshly reset/commissioned switch times out while resolving the lamp, even though the lamp is reachable and works from Home Assistant.

The fallback server (Hardware tab → SRP fallback server, srp_mode) runs an on-device SRP / DNS-SD server, but only while the mesh has no border router; it yields the moment a real one appears, and only one module takes the role (election on RLOC16, lowest wins). The server needs the Router or Leader role, so a module that is only an End Device never runs it.

How it is built: CONFIG_OPENTHREAD_BORDER_ROUTER stays off — turning it on links the prebuilt libopenthread_br.a plus the ESP border-router glue, whose MeshCoP mDNS publisher needs an infra/backbone interface this device does not have, and the module then hangs right after Thread attach on every reboot after commissioning. The SRP/DNS-SD server sources (srp_server.cpp, dnssd_server.cpp) are part of every FTD build though — only the enabling macros sit inside ESP-IDF's #if CONFIG_OPENTHREAD_BORDER_ROUTER block. So the server is enabled through a custom OpenThread header (main/openthread_custom_config.h), which needs no border-router glue at all.

The fallback has no advertising proxy: it fixes discovery inside the mesh (a switch resolving a bound lamp), not discovery of the mesh from your LAN. For that, add a real Thread border router with an advertising proxy (e.g. a Home Assistant OpenThread Border Router on a ZBT-1/SkyConnect dongle).

Switch to WiFi for faster management

The WiFi buttons on the dashboard (WiFi & OTA tab) join WiFi as a station next to a running Thread network, without a reboot:

  • 10 min switches WiFi off again after 10 minutes; pressing it again extends the window.
  • Always on keeps WiFi up permanently and restores it after every reboot (stored in flash). Unlike the 10-minute window it never pauses the Lua scripts, so it needs enough free heap for the WiFi driver next to the configured slots — with many slots use the 10-minute window instead. Only available once the device is commissioned (see below).
  • Off closes the window or ends "always on".

The physical shortcut does exactly the same: press any button 6× rapidly (within 2.5 seconds). Use that when the dashboard is unreachable over Thread. 6× on the PCB button additionally hands GPIO16/17 back to the UART0 console right away (no reboot), so the serial log on J6 shows the WiFi window even when bench mode is off. The Add-on sensors (DS18B20, Analog IN) pause until the next reboot; the SW polarity is not changed. Only do this with the Add-on removed.

Apply on that tab only stores SSID, password, hostname and firmware URL — no reboot. The next window uses them, so a wrong SSID costs a toggle instead of a restart. Restart next to it reboots the device on request.

The 10-minute window works regardless of commissioning status. Without saved WiFi credentials — or when they do not connect within a minute — the window switches to an open SoftAP shelly-cfg-XXXXXX with the dashboard on http://192.168.4.1/ for the remainder of the ten minutes. That is how you reach a module that has never been commissioned, and it keeps a module with stale credentials reachable when Thread is not configured either.

Before commissioning: 10 minutes only, no "Always on"

A module that is not in a Matter fabric yet (fresh install, after a factory reset or Commission Mode) has no Thread network but advertises continuously over BLE for pairing. BLE advertising outranks WiFi in the radio arbiter, so with both on the module associates and even gets an IP address but then hardly anything gets through: no ping, no dashboard, and a laptop on the SoftAP never gets a DHCP lease (it falls back to a 169.254.x.x address). BLE also holds so much heap that next to Matter and the WiFi driver only ~3 kB is left, too little for the dashboard's TCP sends. The firmware therefore shuts BLE down and releases its memory when the WiFi window opens, and reboots when the window closes (after 10 minutes, or via Off): the fresh boot advertises over BLE for commissioning again. A reboot from the dashboard also ends the window, since an uncommissioned module boots without WiFi; press 6× again to reopen it. That is why Always on is refused until the device is commissioned — it would keep BLE, and with it commissioning, off for good — and why a stored "Always on" is downgraded to a 10-minute window at boot as long as the device is uncommissioned (the setting is kept and takes effect again after commissioning). Commission the device first, then switch WiFi to Always on.

With no Thread network to share the radio with, the window before commissioning also keeps the WiFi station awake (no modem sleep) and takes a Thread interface that is still up without a fabric down until the window closes.

What temporary WiFi costs while it is open

WiFi and 802.15.4 share one radio on the ESP32-C6, and Espressif documents only one stable combination: WiFi station next to a Thread End Device (SoftAP next to a Thread Router is listed as unsupported, and SoftAP next to an End Device only as limited once a client is connected). The 10-minute window therefore:

  • starts WiFi as STA (SoftAP when there are no credentials, or when they fail);
  • keeps Thread up, but gives up the router role: no routing for other nodes, no children;
  • in the SoftAP fallback additionally makes Thread a sleepy child, 3 s parent poll). A station has an AP buffering frames for it while the radio serves 802.15.4, a SoftAP has nothing of the kind: with 802.15.4 receiving all the time, clients associate but never get a DHCP lease. Thread stays attached, but mesh traffic is as slow as the poll period until the window closes. Sleepy mode also drops the FTD role and full network data in the same link mode, because OpenThread refuses rx-off-when-idle on a full Thread device. If the stack still refuses, Thread goes down for the rest of the window and comes back at teardown: an unreachable module is worse than a Thread outage that ends by itself;
  • re-arms the coexistence arbiter before every esp_wifi_start(), because stopping WiFi hands the radio back to 802.15.4 — the SoftAP fallback would otherwise run without airtime;
  • stands down the SRP fallback server, which requires Router/Leader;
  • shares the radio, so expect more Thread packet loss while WiFi is busy.

All of that is restored automatically when the window closes — no reboot. If Thread detaches while the window is open, the Thread watchdog closes the window immediately: Thread wins over temporary WiFi — unless the SoftAP fallback took Thread down deliberately, in which case the detached state is expected. The current state is visible on the Hardware tab (Temporary WiFi).

Backup & restore

Via the management dashboard → Backup tab:

  • Download Backup — exports all settings as a JSON file: WiFi credentials + all 8 script slot configurations (name, type, trigger, period, Lua code)
  • Restore Backup — upload a previously downloaded JSON backup to restore all settings. The device reboots automatically after restore.

Factory reset

Via the web management dashboard → Factory Reset button, or by holding the onboard PCB button for 30 seconds (only that button — the wall-switch and Add-on inputs never trigger a reset; the status LED starts blinking fast when the reset is accepted). Both wipe:

  • All NVS data (WiFi credentials, script configurations, bench mode)
  • All Matter fabrics and commissioning data (NVS namespaces)

After factory reset the module reboots into BLE commissioning mode (step 2).

Firmware updates

Once the custom firmware is running you can update it three ways. They all flash the same application binary (build/shelly_gen4_matter_module.bin) — they only differ in transport, and all use standard ESP-IDF OTA slot selection with bootloader rollback (a bad image is automatically reverted to the previous slot).

⚠️ Keep a full UART backup as your guaranteed way back. The management page can flash an original Shelly firmware package back onto the device (see Return to stock below), and that path is verified on a Shelly 1 Gen4 — but it depends on the package matching your device. The one route that always works is restoring the full 8 MB UART backup of that exact device, so make that backup before the first flash (with ESPConnect or esptool.py read_flash, see INSTALL.md) — afterwards it is too late. The factory shelly partition (hardware/Matter credentials) is never overwritten and is preserved across installs.

1. Matter OTA (over Thread)

Update over the existing Thread/Matter connection — no WiFi or cabling needed.

idf.py build
python3 tools/make-matter-ota.py      # → shelly-gen4-matter-module-v<version>.ota
  • And serve it from a Matter OTA provider (e.g. Home Assistant; requires "Enable test-net DCL usage" in the Matter Server add-on, see commissioning):

The image embeds the vendor/product ID and software version; the device only accepts an image with a higher software version than it currently runs.

2. .bin upload via the management dashboard

For modules already running this firmware: open the management dashboard (6× rapid button press → WiFi), go to the WiFi & OTA tab, and either provide a firmware URL or upload build/shelly_gen4_matter_module.bin directly. The device flashes the inactive OTA slot and reboots into it; on boot the app marks itself valid, otherwise the bootloader rolls back.

3. Return to stock Shelly firmware

The management dashboard (Backup tab) can flash an original Shelly firmware package back onto the module — the same .zip the stock web UI consumes. Download the package matching the model on your device label, then upload it under Return to stock Shelly firmware.

The stock app cannot run under our ESP-IDF bootloader (it needs the Shelly OS loader and an SH0S boot state), so the module is made byte-for-byte stock again. The firmware first writes the stock app to the inactive slot and the stock filesystem, and verifies their SHA-256 — nothing outside that inactive slot is touched until this succeeds. It then restores, in order, the stock boot state (otadata), the stock partition table (0x10000), points the SH0S boot-select at the slot the stock app landed in, and finally rewrites the stock bootloader (Shelly OS loader) at 0x0. Every write is verified by read-back. The factory shelly partition is never touched. The units are not flash-encrypted, so the plaintext images from the package reproduce the stock layout exactly.

The restore also erases our nvs partition, so the device comes back up as a factory-fresh stock unit and has to be set up again from scratch.

✅ Verified on a Shelly 1 Gen4: the stock S4SW-001X16EU package restored over the air boots the Shelly OS loader and the stock app again.

⚠️ The bootloader rewrite at 0x0 is the one irreversible step. If it is interrupted (power loss mid-write) the device has no valid loader and needs UART recovery. Keep your full 8 MB UART backup as the guaranteed fallback.

Pin mapping

Onboard Shelly 1 Gen4:

GPIO Function
GPIO4 PCB button — active-low, internal pull-up
GPIO5 Relay output — active-high (high = relay closed)
GPIO10 Pushbutton input / SW terminal — active-high, no internal pull (mains-referenced input circuit). In bench mode it becomes active-low with a pull-up so a plain button to GND works, see BENCH_MODE
GPIO15 Status LED — active-low (low = LED on)

Shelly Plus Add-on (via J6 connector):

GPIO Function
GPIO9 1-Wire TX — DS18B20 commands via ISO7221A isolator. Active-low open-drain signalling (idle high); the isolator does not invert, so driving GPIO9 low pulls the bus low
GPIO16 1-Wire RX — DS18B20 responses via isolator. Active-low, idle high; a presence pulse reads as low
GPIO17 Analog IN — occupancy sensor (e.g. HLK-LD2410S). Active-high PWM duty cycle with internal pull-down; ≥25 % duty (≈2.5 V on the 0–10 V scale) counts as occupied
GPIO18 Digital IN — TTP223 capacitive touch / add-on switch. Active-low, internal pull-up (touch/contact active pulls the pin low). The Add-on terminal itself is active-low too (Shelly specifies −15 V…0.5 V = true, 2.5 V…15 V = false), so the isolator passes the level through uninverted. The stock firmware's "invert digital input" setting does not exist here — invert it in your Lua script if you need the opposite sense

J6 connector pinout (1.27 mm pitch, 7-pin header on back of PCB):

Pin Function GPIO Notes
1 ESP_DBG_UART GPIO18 not used for flashing
2 TXD GPIO16 Shelly TXD → CP2102 RXD
3 RXD GPIO17 Shelly RXD ← CP2102 TXD
4 3.3V — power supply (no 5V!)
5 RESET EN not needed for manual flashing
6 GPIO0 (BOOT) GPIO0 low at power-up → flash mode
7 GND — pin closest to J6 silkscreen

Lua scripting API

Input functions

Function Returns Description
input.button_event() string or nil Last button event (see events table below)
input.button_id() integer Input that triggered the event: 0=SW, 1=Digital IN, 2=PCB button, 3=SW2 (2PM)
input.sw() boolean Current state of SW input (GPIO10)
input.digital() boolean Current state of Digital IN (GPIO18 on the 1 Gen4)
input.device_btn() boolean Current state of PCB button (GPIO4)
input.analog() integer Analog IN duty cycle 0–100 % (GPIO17)
input.temperature() number DS18B20 (Add-on) temperature in °C
input.chip_temperature() number or nil ESP32-C6 internal temperature in °C (all models)
input.board_temperature() number or nil Onboard NTC temperature in °C (1 Mini, 1PM, 2PM; nil on the 1 Gen4)

Button events

Event string Description
"short_press" Short press (< 500ms)
"long_press_start" Long press started
"long_press_stop" Long press released
"double_press" Double press
"short_long_start" Short press followed by long press started
"short_long_stop" Short-long press released
"contact_closed" Button/switch contact closed (pressed)
"contact_open" Button/switch contact opened (released)

Input IDs

The SW / button / Digital-IN GPIOs depend on the selected model (see the hardware table above). The GPIO column below lists the default Shelly 1 Gen4 pins; the 2PM-specific pins are noted separately.

ID Input GPIO
0 SW (1st wall switch) GPIO10 (1 Gen4) — GPIO11 on 2PM
1 Digital IN (add-on) GPIO18 (1 Gen4) — GPIO12 on 1PM, GPIO1 on 2PM
2 PCB button (onboard) GPIO4
3 SW2 (2nd wall switch, 2PM only) GPIO10 on 2PM

Output functions

The relay functions take an optional 1-based channel argument (1 = relay 1, 2 = relay 2 on the 2PM). When omitted, channel 1 is used.

Function Description
output.relay_set(on) Set relay 1 on/off (on = boolean)
output.relay_set(ch, on) Set relay ch (1 or 2) on/off
output.relay(...) Alias for output.relay_set
output.relay_toggle([ch]) Toggle relay ch (default 1)
output.relay_state([ch]) Returns state of relay ch (default 1) as boolean

Script migration note: the relay API is now channel-indexed. Existing single-relay scripts using output.relay_set(true) / output.relay_toggle() / output.relay_state() keep working unchanged (they act on relay 1). Only 2PM scripts that need the second relay must pass a channel: output.relay_set(2, true).

Endpoint functions (client endpoints)

Function Description
endpoint.command("toggle") Send OnOff Toggle to bound devices
endpoint.command("on") Send OnOff On
endpoint.command("off") Send OnOff Off
endpoint.command("move_with_onoff", {up=bool, rate=N}) Start dimming (up=true sends On first; up=false on an off lamp does nothing)
endpoint.command("stop") Stop dimming
endpoint.command("color_temp_set", {mireds=N}) Set color temperature
endpoint.command("color_temp_move", {warmer=bool, rate=N}) Start color temp change
endpoint.command("color_temp_stop") Stop color temp change

Other functions

Function Description
endpoint.set(attr, value) Set sensor attribute (for server endpoints)
log(msg) Print to serial log
timer.millis() Uptime in milliseconds

WiFi behavior

State WiFi Thread/BLE How to reach
Not commissioned OFF ON (BLE commissioning) After flash or factory reset
Commissioned (normal) OFF ON (Thread active) Dashboard over Thread
WiFi next to Thread (management) ON — STA, SoftAP when STA fails Thread active as sleepy End Device (no router role, no SRP fallback; Thread down if sleepy is refused) "10 min" / "Always on" buttons or 6× press — no reboot; 10-min window restores itself, "Always on" survives reboots (commissioned devices only)
WiFi window, not commissioned ON — STA, SoftAP when STA fails No Thread network; BLE shut down, reboot into BLE commissioning when the window closes "10 min" button or 6× press only — "Always on" is refused until commissioned

Status LED

The onboard status LED (GPIO15) indicates the device state:

Pattern Description
Fast blink (5 Hz) Boot / initialization in progress, or OTA update active
Slow blink (1 Hz) Not commissioned — waiting for BLE pairing
Heartbeat (short flash every 2s) Normal operation — commissioned and online
Off LED disabled or no pattern set

During boot the LED blinks fast. After initialization it switches to heartbeat (if commissioned) or slow blink (if not yet commissioned).

Unicast binding reachability

A unicast binding sends over a cached CASE session, and that session holds the IPv6 address the peer had when the session was created. Thread addresses are not stable — a border router restart hands out a new on-mesh prefix, and a parent change gives a node a new RLOC — so the address can go stale while the session still looks healthy. Sending then goes nowhere and MRP retries for ~35 s.

Three mechanisms keep that off the critical path:

Mechanism What it does
Invoke response timeout (1.5 s) After a timeout the session is evicted and the command is retried once over a fresh CASE session — which does a new DNS-SD lookup. Skipped when the peer acknowledged the first attempt, because Toggle is not idempotent.
Thread network-data watch Automatic. When the set of on-mesh prefixes changes, every cached session to a bound peer is evicted right away, because all peer addresses just expired.
Binding keepalive Configurable. Reads ClusterRevision from each bound peer's bound cluster every N seconds and drops the session when that read fails, so the failure is discovered in the background instead of on a button press.

The keepalive interval is set on the Hardware tab or over HTTP; 0 disables it, the minimum is 60 s. Default is 600 s.

BENCH_MODE

Controls GPIO10 polarity and sensor initialization. Configurable at runtime via the management dashboard.

BENCH_MODE GPIO10 Sensors Use case
0 Active-high (230V optocoupler) Active Production
1 (default) Active-low + pull-up Skipped (UART0 stays active) Development

To get the serial log without switching bench mode (for example when the dashboard cannot be reached), press the PCB button 6×: UART0 comes back immediately and stays until the next reboot, see Switch to WiFi for faster management.

Build + flash

File structure

shelly_gen4_matter_module/
├── CMakeLists.txt
├── sdkconfig.defaults
├── main/
│   ├── app_config.h        # pins, timings, BENCH_MODE
│   ├── app_main.cpp        # boot sequence, smart boot logic
│   ├── matter_device.cpp   # dynamic endpoint creation + command emit
│   ├── script_engine.c     # Lua 5.4 scripting engine
│   ├── hw_config.c/.h      # runtime hardware profile (1/Mini/1PM/2PM)
│   ├── button.c/.h         # button driver (wall switches, PCB button, gestures)
│   ├── relay.c/.h          # relay GPIO control (1 or 2 channels)
│   ├── power_meter.c/.h    # BL0942 driver (1PM Gen4)
│   ├── ade7953.c/.h        # ADE7953 dual-channel driver (2PM Gen4)
│   ├── sensors.c/.h        # DS18B20 + analog occupancy
│   ├── ota.c/.h            # WiFi runtime, management dashboard, OTA
│   ├── status_led.c/.h     # LED patterns
│   ├── secrets.h           # compile-time WiFi credentials (gitignored)
│   └── CHIPProjectConfig.h # vendor/product name overrides
├── components/lua/         # Lua 5.4 as ESP-IDF component
├── tools/
│   ├── make-matter-ota.py       # build Matter OTA image (.ota) — see Firmware updates
│   ├── make-webui-ota-zip.py    # build Shelly Stock web-UI OTA zip
│   ├── make_factory_bin_file.sh # merge binaries for UART/ESPConnect flashing
│   ├── create_matter_cluster_group.py  # set up multicast group + bindings
│   └── shelly-overview.sh       # clickable HTML overview of modules via mDNS
├── SCRIPTS.md              # example Lua scripts
├── INSTALL.md
└── INSTALL_VSCODE_WINDOWS.md

Known limitations

  • Test vendor ID: firmware uses vendor ID 0xFFF1. For Google/Apple Home publication a CSA vendor ID is required.
  • Test DAC: for production, provision real Device Attestation Certificates in the NVS chip-factory namespace. For local HA usage the test DAC works fine once "Enable test-net DCL usage" is on in the Matter Server add-on.

License

Espressif esp-matter and connectedhomeip: Apache 2.0. Lua: MIT. This custom code: MIT.

About

Open-source Matter over Thread firmware for Shelly 1 Gen4, 1 Mini Gen4, 1PM Gen4 and 2PM Gen4 (ESP32-C6). One image for all four, Lua-scriptable endpoints, power metering, Add-on sensors, Matter OTA. No cloud, no WiFi needed. Home Assistant, Apple Home, Google Home.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages