Skip to content
Matthias Jobst edited this page May 15, 2026 · 3 revisions

ESPSomfy-RTS with HomeKit — Wiki

ESPSomfy-RTS with HomeKit is an ESP32 firmware that controls Somfy RTS motorised blinds via a CC1101 RF transceiver and exposes them natively to Apple HomeKit. It runs on ESP-IDF v5.5 with arduino-esp32 as a managed component, serves a web interface on port 80, a REST API on port 8081, and a WebSocket event bus on port 8080.


Table of contents

  1. Hardware requirements
  2. First flash
  3. First boot and WiFi setup
  4. Web interface
  5. Adding and pairing shades
  6. Controlling shades
  7. HomeKit
  8. REST API
  9. WebSocket events
  10. MQTT
  11. OTA updates
  12. Backup and restore
  13. Building from source
  14. CC1101 driver notes
  15. Troubleshooting

Hardware requirements

Supported targets

Target Flash Notes
ESP32-S3 8 MB Primary target; debug and release builds
ESP32 4 MB Release build only (optimized binary fits in 1.5 MB OTA slot)

Bill of materials

  • ESP32-S3 or ESP32 development board (8 MB or 4 MB flash)
  • CC1101 RF transceiver module (433 MHz)
  • Jumper wires

CC1101 wiring (ESP32-S3)

CC1101 pin ESP32-S3 GPIO
GDO0 GPIO 3
GDO2 GPIO 4
CSN GPIO 6
SCK GPIO 7
MISO GPIO 8
MOSI GPIO 9
VCC 3.3 V
GND GND

For ESP32 (classic) the GPIO assignments can be changed in the web interface under Settings → Transceiver.


First flash

Download the two release assets for your chip from the GitHub releases page and flash them with esptool.py or the Espressif Flash Download Tool:

ESP32-S3 (8 MB):

Asset Address
SomfyController.esp32s3.bin 0x10000
SomfyController.littlefs.bin 0x610000
esptool.py --chip esp32s3 --port /dev/cu.usbmodem0 write_flash \
    0x10000  SomfyController.esp32s3.bin \
    0x610000 SomfyController.littlefs.bin

ESP32 (4 MB):

Asset Address
SomfyController.esp32.bin 0x10000
SomfyController.littlefs.esp32.bin 0x310000
esptool.py --chip esp32 --port /dev/cu.usbmodem0 write_flash \
    0x10000  SomfyController.esp32.bin \
    0x310000 SomfyController.littlefs.esp32.bin

First boot and WiFi setup

On first boot — or whenever no WiFi credentials are stored — the device opens a soft access point:

  • SSID: ESPSomfyRTS (or your configured hostname)
  • Password: (none — open network)
  • Web UI: http://192.168.4.1

Connect to the access point from any phone or computer, open the web UI, and go to Settings → Network to enter your home WiFi credentials. After saving, the device reboots, connects to your network, and is reachable at http://ESPSomfyRTS.local (mDNS) or by IP address.

The soft AP closes automatically once a station connects to your WiFi. If the configured SSID cannot be found the AP reopens so you can reconfigure.


Web interface

The web UI is served from LittleFS on port 80. The main sections are:

Section URL Purpose
Shades / Live position, tilt, and control of all shades
Rooms / (room view) Shades grouped by room
Groups / (group view) Control multiple shades simultaneously
Settings / → Settings Network, security, MQTT, transceiver, NTP
HomeKit / → HomeKit Pairing code, QR code, paired/unpaired status, reset pairings
OTA / → Firmware Select a GitHub release and flash over the air

The current firmware version (from git describe) is shown in the settings page.


Adding and pairing shades

Somfy RTS uses one-way radio: the controller sends commands; shades do not reply. Adding a shade means registering its address in the firmware and pairing the firmware's virtual remote with the motor.

  1. Add a shade — In the web UI, go to Shades → Add Shade and fill in the name and motor type. The firmware assigns a new rolling-code address.
  2. Enter programming mode on the motor — Use the physical Somfy remote that is already paired with the motor: hold the PROG button until the shade jogs (moves briefly up then down).
  3. Send PROG from the web UI — On the new shade's card, press Prog. The shade jogs again to confirm pairing.
  4. Set limits — Drive the shade to the fully-open and fully-closed positions using Up/Down, then save them as limits if your motor supports it.
  5. Set My position — Drive to the desired favourite position and press My to store it.

To link a secondary remote (so commands from an existing physical remote are mirrored to the shade's position tracking), use Settings → Linked Remotes on the shade.


Controlling shades

Commands

Command Description
Up Raise / open
Down Lower / close
Stop Stop movement
My Move to stored My (favourite) position
Prog Enter/exit motor programming mode
Favorite Move to favourite (motor-stored) position
StepUp Nudge up one step
StepDown Nudge down one step
Toggle Reverse current direction
MyUp My + Up (wakes hibernating motors)
SunFlag Sun-sensor trigger

Position (move-to-target)

Instead of a command, you can send a target position (0–100, where 0 = closed, 100 = open). The firmware calculates the required travel time from the calibrated limits and stops the motor at the right moment using My. This is how HomeKit and the position slider in the web UI work.


HomeKit

The firmware uses the esp-homekit-sdk to expose each shade as a Window Covering accessory.

Pairing

The 8-digit setup code is derived from the device's MAC address on first boot and persisted to NVS. Find it in the web UI under HomeKit: it is displayed as a QR code and as a plain number.

  1. Open the Home app on iPhone/iPad.
  2. Tap +Add AccessoryMore options…
  3. Scan the QR code shown in the web UI, or enter the 8-digit code manually.
  4. Each shade appears as a separate Window Covering tile.

The web UI shows Paired / Not paired status. Use Reset Pairings (web UI or POST /homekit/resetPairings) to start over.

Characteristics exposed per shade

Characteristic Notes
Current Position 0–100 %
Target Position 0–100 % — triggers move-to-target
Position State Decreasing / Increasing / Stopped
Hold Position Stops movement when set to true

mDNS note

Apple HomeKit overwrites the mDNS hostname during HAP initialisation. The firmware re-asserts your configured hostname immediately after HomeKit starts so that ESPSomfyRTS.local continues to resolve correctly.


REST API

The REST API is available on both port 80 (shared with the web UI) and port 8081 (REST only, no HTML pages). All requests and responses use JSON. Authentication, if enabled, uses HTTP Basic Auth.

Shade commands

POST /shadeCommand
Content-Type: application/json

{ "shadeId": 1, "command": "Up" }
POST /shadeCommand
Content-Type: application/json

{ "shadeId": 1, "target": 75 }

target is 0–100 (0 = closed). command is one of the strings in the commands table above.

Tilt commands

POST /tiltCommand
Content-Type: application/json

{ "shadeId": 1, "command": "Up" }

Group commands

POST /groupCommand
Content-Type: application/json

{ "groupId": 1, "command": "Down" }

Repeat last command

POST /repeatCommand
Content-Type: application/json

{ "shadeId": 1 }

Query endpoints

Method Path Description
GET /shades All shades as a JSON array
GET /shade?shadeId=N Single shade
GET /rooms All rooms
GET /room?roomId=N Single room
GET /groups All groups
GET /group?groupId=N Single group
GET /controller Controller info (version, chip, IP, uptime)

Sensor state

POST /setSensor
Content-Type: application/json

{ "shadeId": 1, "sunny": true, "windy": false }

sunny and windy accept a boolean or integer (0/1). Either shadeId or groupId may be supplied.

Set positions (bulk)

POST /setPositions
Content-Type: application/json

{ "shades": [ { "shadeId": 1, "position": 50 }, { "shadeId": 2, "position": 0 } ] }

WebSocket events

The device pushes real-time state updates to all connected clients over a WebSocket on port 8080. Events are JSON objects with an evt field identifying the type.

Common events:

evt Payload Fired when
shadeState shade object Position or flags change
groupState group object Group state changes
controllerState controller object Network or version changes

Connect with any WebSocket client to ws://<device-ip>:8080.


MQTT

Enable MQTT in Settings → MQTT. The firmware publishes shade state and subscribes to command topics using a PubSubClient-based driver.

Setting Default Description
Broker hostname MQTT broker address
Port 1883 Broker port (use 8883 for TLS)
Username / Password Optional credentials
Root topic Prefix for all topics
Home Assistant discovery off Publish HA MQTT discovery messages
Discovery topic homeassistant HA discovery prefix

When Home Assistant discovery is enabled, the firmware publishes a config payload to homeassistant/cover/<shadeId>/config on connect, making shades appear automatically in HA.


OTA updates

  1. In the web UI, go to Firmware.
  2. Select a release from the dropdown (release notes are shown inline).
  3. Click Download & Install. The firmware downloads and flashes SomfyController.<chip>.bin and SomfyController.littlefs<.chip>.bin from GitHub, then reboots.

No serial cable is needed after the initial flash. OTA uses the two-slot partition scheme: the currently running slot is never overwritten, so a failed download leaves the device bootable.

You can also trigger OTA via the REST API:

POST /downloadFirmware
Content-Type: application/json

{ "ver": "v0.4.4" }

Backup and restore

Download a backup:

GET /backup

The response is a JSON file containing all shade, room, group, and settings data. Save it somewhere safe before updating firmware.

Restore from backup: Use Settings → Restore in the web UI to upload a backup file. You can choose which sections to restore (shades, rooms, groups, network settings, transceiver settings).


Building from source

Build environment

The project targets ESP-IDF v5.5 with arduino-esp32 v3.x embedded as a managed component (not the Arduino IDE). app_main() calls initArduino() directly so Arduino APIs work without the Arduino task scheduler.

idf.py build
idf.py -p /dev/cu.usbmodem<PORT> flash monitor

ESP32-S3 release build (optimizations enabled, debug logs suppressed — matches the binary attached to GitHub releases):

idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.release" build

ESP32 (4 MB) release build:

idf.py set-target esp32
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.esp32;sdkconfig.release" build

Static analysis (requires a completed build for compile_commands.json):

# Via idf.py (slower, uses the bundled run-clang-tidy):
idf.py clang-check \
  --exclude-paths components \
  --run-clang-tidy-options '-header-filter=.*/SomfyController/main/.*'

# Via the project script (faster, covers headers too):
python3 run-clang-tidy.py

Unit tests

Tests live in test/unit/ and run on the host (macOS/Linux) using GoogleTest + gmock. They cover the full shade object model (SomfyShade, SomfyGroup, SomfyRoom, SomfyShadeController) and the transceiver (RX state machine, RMT TX encoder, loop integration). ESP-IDF and Arduino APIs are replaced by stubs — no hardware required.

cd test/unit

# Configure (first time or after CMakeLists changes)
cmake -B build -DCMAKE_BUILD_TYPE=Debug -S .

# Build and run all tests
cmake --build build --parallel && ./build/shade_tests

# Run a single suite
./build/shade_tests --gtest_filter='CommandTransmitterTest.*'

# Coverage report
cmake --build build --parallel && ./build/shade_tests
gcovr --object-directory build --filter '.*main/somfy/'

CI/CD

Three jobs run on every push and pull request:

Job What it does
Build Compiles the firmware for ESP32-S3 with ESP-IDF via espressif/esp-idf-ci-action; uploads SomfyController.bin as a workflow artifact.
Unit Tests Builds and runs the GoogleTest suite on Ubuntu; generates a gcovr HTML coverage report as an artifact.
LittleFS Builds the web-asset filesystem image with mklittlefs and uploads SomfyController.littlefs.bin as an artifact.

When a GitHub release is published, the release workflow additionally builds the ESP32 (4 MB) target and attaches all four binaries (firmware + LittleFS for each chip) to the release.


CC1101 driver notes (arduino-esp32 v3.x)

The upstream SmartRC-CC1101-Driver-Lib was written for arduino-esp32 v2.x. The copy in components/CC1101/ contains three fixes required for v3.x:

  1. SPI.begin(..., -1) — pass -1 for the SS argument so the pin is not routed through the hardware CS peripheral, allowing Reset() to strobe it manually via digitalWrite.
  2. SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)) added to SpiStart() — on v3.x SPI.transfer() without a prior beginTransaction() does not configure the hardware clock and hangs indefinitely.
  3. SpiEnd() is called immediately after Reset() in Init(), before RegConfigSettings() — this prevents a mutex deadlock caused by RegConfigSettings()SpiWriteReg()SpiStart()beginTransaction() trying to re-lock the non-reentrant SPI mutex while the outer Init() still holds it.

Troubleshooting

Device not found at ESPSomfyRTS.local

mDNS can be unreliable on some networks. Find the IP address from your router's DHCP table and access the device by IP directly. You can change the hostname in Settings → Network.

HomeKit not responding / shade stuck

HomeKit sends commands over Wi-Fi. If the Wi-Fi connection is unstable the HAP session can drop. Check the device log (idf.py monitor) for disconnection events. Disabling TX A-MPDU aggregation in sdkconfig.defaults (CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=n) has resolved frame-drop issues on congested networks.

Shade position drifts after OTA

Rolling codes are stored in NVS and survive OTA. If position is wrong after an update, re-calibrate the limits: drive to the fully-open position, save as 100 %; drive to fully-closed, save as 0 %.

Motor does not respond to commands

  • Confirm the CC1101 wiring matches the pin table above.
  • Check the RF frequency in Settings → Transceiver. European Somfy RTS motors use 433.42 MHz; US models may use 433.92 MHz.
  • Use the Frequency scan feature to detect the frequency your existing remote uses.

Core dump on serial after crash

The firmware emits a base64-encoded ELF core dump on panic. Decode it with:

idf.py coredump-info --core <(base64 -d <<< "<base64 block from serial>")