- C++ 93.2%
- Python 6.8%
Native ESPHome custom component to control io-homecontrol 1-way roller shutters (Velux, Somfy, etc.) with an ESP32-S3 + Semtech SX1276 FSK transceiver (e.g. LilyGo T3-S3). Features: native Cover entity, reliable 1W TX with AES-128 HMAC and a wake-up preamble + short repeats, physical remote eavesdropping, remote sniffing/discovery with YAML export, OLED status, and NVS-persisted rolling sequence counters. |
||
|---|---|---|
| components/iohc | ||
| packages | ||
| .gitignore | ||
| LICENSE | ||
| README.md | ||
| secrets.yaml.example | ||
| shutter.yaml | ||
ESPHome io-homecontrol (iohc) Shutter Component
Native, lightweight ESPHome custom component for controlling io-homecontrol 1-way (1W) roller shutters using an ESP32-S3 + Semtech SX1276 LoRa/FSK transceiver (e.g., LilyGo T3-S3). Developed and verified against VELUX roller shutters; io-homecontrol 1-way is a shared protocol, so other io-homecontrol brands (Somfy, etc.) are expected to work as well.
Features
- Native ESPHome Cover Entity: Full Open, Close, Stop, and Position slider (0–100%) controls directly in Home Assistant and via the built-in Web UI.
- Reliable 1W Transmission: Implements the official io-homecontrol 1W framing with AES-128 HMAC, a 1920-symbol (~500 ms) wake-up preamble, and 4 timed repeats (5 packets total) with 40 ms inter-packet spacing.
- Priority Stop Preemption: Asynchronous non-blocking state machine in
loop()ensures Stop commands immediately preempt active movement commands with <5 ms API response latency. - Physical Remote Eavesdropping: Continuously listens on 868.95 MHz (Channel 2) for paired wall remotes and updates Home Assistant position state in real-time.
- Remote Sniffing & Discovery: Built-in sniffer automatically captures unknown physical remotes in the air, wakes the OLED with the remote address and current rolling sequence, and outputs ready-to-copy YAML blocks in ESPHome / Home Assistant logs.
- OLED Display with Auto-Off: Live status feedback on LilyGo T3-S3 onboard SSD1306 OLED (wakes on outgoing commands, physical remotes, sniffing, and movement; cleanly powers off after 10s of inactivity to prevent burn-in).
- Persistent Sequence Numbers: Rolling sequence counters are saved to ESP-IDF non-volatile storage (NVS) across reboots and power outages.
- Time-based Position Tracking: Accurately tracks position based on calibrated travel times, auto-calibrating whenever the shutter reaches 0% or 100%.
Hardware Pinout (LilyGo T3-S3)
| Peripheral / Pin | ESP32-S3 GPIO | Description |
|---|---|---|
| SX1276 SCK | GPIO 5 |
Hardware SPI Bus 2 (SPI2_HOST) |
| SX1276 MISO | GPIO 3 |
Hardware SPI Bus 2 |
| SX1276 MOSI | GPIO 6 |
Hardware SPI Bus 2 |
| SX1276 NSS / CS | GPIO 7 |
Radio Chip Select |
| SX1276 RST | GPIO 8 |
Radio Hardware Reset |
| SX1276 DIO0 | GPIO 9 |
PacketSent (TX) / PayloadReady (RX) Interrupt |
| OLED SDA | GPIO 18 |
Hardware I2C (SSD1306 128x64 @ 0x3C) |
| OLED SCL | GPIO 17 |
Hardware I2C (SSD1306 128x64 @ 0x3C) |
| Boot Button | GPIO 0 |
Hardware button to trigger Sniff mode (30s) |
Quick Start
1. Configure Secrets
Copy the example secrets file:
cp secrets.yaml.example secrets.yaml
Edit secrets.yaml with your Wi-Fi credentials and 16-byte (32 hex character) shutter encryption keys:
wifi_ssid: "Your_WiFi_SSID"
wifi_password: "Your_WiFi_Password"
ap_ssid: "rolladen"
ap_password: "fallback_password"
key_shutter_1: "000102030405060708090a0b0c0d0e0f"
key_shutter_2: "0f0e0d0c0b0a09080706050403020100"
2. Configure Your Shutters (shutter.yaml)
On the LilyGo T3-S3, all hardware SPI and SX1276 pins are configured automatically by default. The OLED display logic is cleanly isolated in packages/display_t3s3.yaml. You only need:
esphome:
name: shutter
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
external_components:
- source: components
packages:
display: !include packages/display_t3s3.yaml # Optional: comment out if no OLED
iohc:
cover:
- platform: iohc
name: "Shutter 1"
node: 0xAABBCC
key: !secret key_shutter_1
open_duration: 33s
close_duration: 33s
initial_sequence: 10
eavesdrop_remote: 0x112233
- platform: iohc
name: "Shutter 2"
node: 0xDDEEFF
key: !secret key_shutter_2
open_duration: 32s
close_duration: 32s
initial_sequence: 10
eavesdrop_remote: 0x445566
Pin Overrides for Other Boards (Optional)
If you are using a custom board or different pinout, you can easily override pins directly under iohc: or provide your own spi: bus:
# Direct pin overrides under iohc:
iohc:
cs_pin: GPIO10
rst_pin: GPIO11
dio0_pin: GPIO12
clk_pin: 13
miso_pin: 14
mosi_pin: 15
Or connect iohc: to a pre-existing SPI bus:
spi:
id: my_spi
clk_pin: GPIO18
miso_pin: GPIO19
mosi_pin: GPIO23
iohc:
spi_id: my_spi
cs_pin: GPIO5
rst_pin: GPIO4
dio0_pin: GPIO2
3. Compile and Upload
# Compile
uvx esphome compile shutter.yaml
# Upload Over-The-Air (OTA)
uvx esphome upload --device <device-ip> shutter.yaml
# Live Logs
uvx esphome logs --device <device-ip> shutter.yaml
Onboarding & Pairing Flows (3 Simple Steps)
In io-homecontrol, roller shutter motors store up to 9–16 independent remote transmitters. You do not need to extract or clone the secret key of your existing physical wall remote! Instead, the ESP32 registers itself as a new, independent virtual remote with its own rolling sequence counter and a newly generated 16-byte AES key.
All pairing configurations remain 100% declarative and portable in your YAML files — no magic hidden keys stored exclusively in NVS flash!
flowchart TD
A["<b>Schritt 1 (optional): Wandschalter belauschen</b><br/>• Klick auf 'Sniff Remote' in HA<br/>• Taste an Wandschalter drücken<br/>• ESP32 merkt sich: 'Wandschalter = 0x112233'"] --> B["<b>Schritt 2: Motor anlernen (Pairing)</b><br/>• PROG auf Rückseite des Wandschalters 2s drücken (Motor nickt)<br/>• Klick auf 'Pair New Device' in HA<br/>• ESP32 generiert frische Node-ID + 16-Byte AES-Key<br/>• ESP32 sendet 0x30 Anlernpaket an Motor (Motor nickt)"]
B --> C["<b>Schritt 3: Fertige Config herunterladen</b><br/>• http://shutter.local/discovered.yaml aufrufen<br/>• Enthält fertigen Block inkl. secrets.yaml & cover: Eintrag<br/>• In secrets.yaml & shutter.yaml einfügen -> Fertig!"]
Schritt-für-Schritt Anleitung:
- Schritt 1 (optional): Wandschalter sniffen für Status-Tracking:
- Gehe in Home Assistant auf dein Shutter-Gerät in den Bereich Konfiguration und klicke auf Sniff Remote (oder öffne die ESPHome Web-UI unter
http://shutter.local). - Drücke eine beliebige Taste (z. B. STOP) auf deiner physischen Fernbedienung (Smoove, Situo etc.).
- Das OLED-Display und das Log zeigen sofort die erkannte Fernbedienung (z. B.
0x112233).
- Gehe in Home Assistant auf dein Shutter-Gerät in den Bereich Konfiguration und klicke auf Sniff Remote (oder öffne die ESPHome Web-UI unter
- Schritt 2: Rollladen-Motor in den Anlernmodus versetzen & koppeln:
- Drücke auf der Rückseite deiner physischen Fernbedienung die PROG-Taste für 2 Sekunden, bis der Rollladen kurz auf- und abruckt (Klack-Klack / Jog).
- Klicke in Home Assistant (unter Konfiguration) auf Pair New Device.
- Der ESP32 generiert eine neue 3-Byte Node-ID, einen frischen 16-Byte AES-Zufallsschlüssel und sendet das Somfy
0x30Key-Exchange-Paket (verschlüsselt mit dem Somfy-Master-Schlüssel). - Das Display zeigt
PAIRING SENT!, und der Rollladen ruckt erneut zur Bestätigung!
- Schritt 3: Fertige Konfiguration herunterladen:
- Öffne in deinem Browser:
http://shutter.local/discovered.yaml. - Die heruntergeladene Datei liefert dir die vollständigen, einsatzbereiten Blöcke:
# --- STEP 1: Add to secrets.yaml --- key_shutter_1: "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d" # --- STEP 2: Add under cover: in shutter.yaml --- cover: - platform: iohc name: "Rolladen 1" node: 0xA1B2C3 key: !secret key_shutter_1 open_duration: 30s close_duration: 30s initial_sequence: 10 eavesdrop_remote: 0x112233 # Status: PAIRED via 0x30 Key Exchange - Kopiere den Key in deine
secrets.yamlund den Block in deineshutter.yaml. - Nach dem nächsten OTA-Upload ist dein Rollladen fest integriert!
- Öffne in deinem Browser:
Home Assistant Entitäten & Dashboard-Hygiene
Um dein Home Assistant Dashboard übersichtlich und sauber zu halten, sind alle Hilfs- und Setup-Entitäten mit passenden Entity-Kategorien versehen:
| Entität | Kategorie | Beschreibung |
|---|---|---|
cover.shutter_1 |
(Hauptansicht) | Reguläre Rollladen-Steuerung (Auf, Ab, Stop, Position 0–100%). |
button.sniff_remote |
config (Konfiguration) |
Startet 30 Sekunden Sniffer-Modus zum Erkennen von Wandschaltern. |
button.pair_new_device |
config (Konfiguration) |
Koppelt einen neuen Rollladen per 0x30 Key-Add-Frame. |
button.clear_discovered_devices |
config (Konfiguration) |
Leert die interne Discovery-Liste. |
sensor.discovered_devices_count |
diagnostic (Diagnose) |
Anzahl aktuell erkannter/gekoppelter Geräte. |
text_sensor.discovered_yaml_download_url |
diagnostic (Diagnose) |
Direkter Download-Link zur erzeugten YAML-Datei. |
Tip
Die Setup-Buttons erscheinen auf der Home Assistant Geräteseite aufgeräumt im Bereich „Konfiguration“ und stören nicht auf deinem Haupt-Dashboard.
Project Structure
├── components/iohc/ # ESPHome custom component
│ ├── __init__.py # Hub configuration schema
│ ├── cover.py # Cover entity schema
│ ├── iohc_hub.h/.cpp # SX1276 FSK driver & non-blocking TX/RX engine
│ ├── iohc_cover.h/.cpp # Native Cover entity with position tracking
│ ├── iohc_crypto.h/.cpp # mbedTLS AES-128 HMAC calculation
│ └── sx1276_regs.h # SX1276 FSK register definitions
├── packages/
│ └── display_t3s3.yaml # Modular LilyGo T3-S3 OLED display package
├── shutter.yaml # ESPHome main node configuration
├── secrets.yaml.example # Example secrets template
├── .gitignore # Ignores build artifacts (.esphome/) and secrets.yaml
└── README.md
Credits & Prior Work
This ESPHome component is a ground-up rewrite, but it stands on the shoulders of the io-homecontrol reverse-engineering and firmware work that came before it. Huge thanks to:
- Velocet/iown-homecontrol — the community protocol documentation and reverse-engineering effort that makes any of this possible.
- cridp/iown-homecontrol-esp32sx1276 — the ESP32 + Semtech SX1276 1W/2W firmware (Apache-2.0, © 2024 CRIDP) that the framing, AES-128 HMAC and radio handling here are derived from and validated against.
- rspaargaren/iohomecontrol — the PlatformIO project (with Dutch/English wiki) focused on 1W shutter control, which this component was ported from.
If you find this useful, consider supporting the upstream authors.
License
Licensed under the Apache License, Version 2.0, consistent with the
upstream projects this work derives from. See LICENSE.