-
Notifications
You must be signed in to change notification settings - Fork 0
Home
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.
- Hardware requirements
- First flash
- First boot and WiFi setup
- Web interface
- Adding and pairing shades
- Controlling shades
- HomeKit
- REST API
- WebSocket events
- MQTT
- OTA updates
- Backup and restore
- Building from source
- CC1101 driver notes
- Troubleshooting
| 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) |
- ESP32-S3 or ESP32 development board (8 MB or 4 MB flash)
- CC1101 RF transceiver module (433 MHz)
- Jumper wires
| 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.
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.binESP32 (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.binOn 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.
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.
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.
- 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.
- 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).
- Send PROG from the web UI — On the new shade's card, press Prog. The shade jogs again to confirm pairing.
- 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.
- 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.
| 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 |
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.
The firmware uses the esp-homekit-sdk to expose each shade as a Window Covering accessory.
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.
- Open the Home app on iPhone/iPad.
- Tap + → Add Accessory → More options…
- Scan the QR code shown in the web UI, or enter the 8-digit code manually.
- 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.
| 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 |
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.
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.
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.
POST /tiltCommand
Content-Type: application/json
{ "shadeId": 1, "command": "Up" }
POST /groupCommand
Content-Type: application/json
{ "groupId": 1, "command": "Down" }
POST /repeatCommand
Content-Type: application/json
{ "shadeId": 1 }
| 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) |
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.
POST /setPositions
Content-Type: application/json
{ "shades": [ { "shadeId": 1, "position": 50 }, { "shadeId": 2, "position": 0 } ] }
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.
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.
- In the web UI, go to Firmware.
- Select a release from the dropdown (release notes are shown inline).
- Click Download & Install. The firmware downloads and flashes
SomfyController.<chip>.binandSomfyController.littlefs<.chip>.binfrom 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" }
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).
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 monitorESP32-S3 release build (optimizations enabled, debug logs suppressed — matches the binary attached to GitHub releases):
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.release" buildESP32 (4 MB) release build:
idf.py set-target esp32
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.esp32;sdkconfig.release" buildStatic 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.pyTests 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/'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.
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:
-
SPI.begin(..., -1)— pass-1for the SS argument so the pin is not routed through the hardware CS peripheral, allowingReset()to strobe it manually viadigitalWrite. -
SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0))added toSpiStart()— on v3.xSPI.transfer()without a priorbeginTransaction()does not configure the hardware clock and hangs indefinitely. -
SpiEnd()is called immediately afterReset()inInit(), beforeRegConfigSettings()— this prevents a mutex deadlock caused byRegConfigSettings()→SpiWriteReg()→SpiStart()→beginTransaction()trying to re-lock the non-reentrant SPI mutex while the outerInit()still holds it.
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 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.
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 %.
- 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.
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>")