A custom Home Assistant integration for Rademacher DuoFern devices via the DuoFern USB stick.
Communicates directly with the USB stick using the native serial protocol — no cloud, no gateway, fully local.
Forked from @MSchenkl and extensively rewritten to aim for a complete re-implementation based on the FHEM modules 10_DUOFERNSTICK.pm and 30_DUOFERN.pm, aiming for near-complete feature parity with the FHEM DuoFern module.
| Description | Code | HA Platform | Tested |
|---|---|---|---|
| RolloTron Standard | 0x40 |
cover |
✅ |
| RolloTron Comfort Slave | 0x41 |
cover |
❌ |
| Rohrmotor-Aktor | 0x42 |
cover |
✅ |
| Rohrmotor Steuerung | 0x47 |
cover |
❌ |
| Rohrmotor | 0x49 |
cover |
✅ |
| Connect-Aktor | 0x4B |
cover |
❌ |
| Troll Basis | 0x4C |
cover |
❌ |
| SX5 (Garage Door) | 0x4E |
cover |
❌ |
| RolloTron Comfort Master | 0x61 |
cover |
✅ |
| Troll Comfort DuoFern | 0x70 |
cover |
❌ |
| Description | Code | HA Platform | Tested |
|---|---|---|---|
| Universalaktor (2-channel) | 0x43 |
switch |
✅ |
| Steckdosenaktor (also Universalaktor 1-Channel) | 0x46 |
switch |
✅ |
| Troll Comfort DuoFern (Lichtmodus) | 0x71 |
switch |
❌ |
| Description | Code | HA Platform | Tested |
|---|---|---|---|
| Dimmaktor | 0x48 |
light |
❌ |
| Dimmer (9476-1) | 0x4A |
light |
❌ |
| Description | Code | HA Platform | Tested |
|---|---|---|---|
| Raumthermostat | 0x73 |
climate |
✅ |
| Heizkörperantrieb | 0xE1 |
climate |
✅ |
| Description | Code | HA Platform | Tested |
|---|---|---|---|
| Bewegungsmelder | 0x65 |
binary_sensor |
❌ |
| Rauchmelder | 0xAB |
binary_sensor |
✅ |
| Fenster-Tür-Kontakt | 0xAC |
binary_sensor |
✅ |
| Umweltsensor | 0x69 |
sensor |
❌ |
| Sonnensensor | 0xA5 |
binary_sensor |
✅ |
| Sonnensensor (alt) | 0xAF |
binary_sensor |
❌ |
| Sonnen-/Windsensor | 0xA9 |
binary_sensor |
❌ |
| Markisenwaechter | 0xAA |
binary_sensor |
❌ |
| Description | Code | Notes | Tested |
|---|---|---|---|
| Wandtaster | 0xA4 |
Fires duofern_event on the HA event bus |
❌ |
| Wandtaster 6fach 230V | 0x74 |
Fires duofern_event on the HA event bus |
❌ |
| Wandtaster 6fach Bat | 0xAD |
Fires duofern_event on the HA event bus |
❌ |
| Funksender UP | 0xA7 |
Fires duofern_event on the HA event bus |
❌ |
| Handsender (6 Gruppen / 48 Geräte) | 0xA0 |
Fires duofern_event on the HA event bus |
✅ |
| Handsender (1 Gruppe / 48 Geräte) | 0xA1 |
Fires duofern_event on the HA event bus |
❌ |
| Handsender (6 Gruppen / 1 Gerät) | 0xA2 |
Fires duofern_event on the HA event bus |
❌ |
| Handsender (1 Gruppe / 1 Gerät) | 0xA3 |
Fires duofern_event on the HA event bus |
❌ |
| HomeTimer | 0xA8 |
Fires duofern_event on the HA event bus |
❌ |
| Handzentrale | 0xE0 |
Fires duofern_event on the HA event bus |
❌ |
USB Stick: Rademacher DuoFern USB-Stick 7000 and 9000 (VID: 0x0403, PID: 0x6001)
- Open / Close / Stop — standard movement commands
- Set Position — move to any position (0–100 %)
- Dusk position button — move to the device's programmed dusk position using the device's built-in speed profile. Equivalent to
set DEVICE duskin FHEM. - Dawn position button — move to the device's programmed dawn position. Equivalent to
set DEVICE dawnin FHEM. - Toggle button — reverse current movement / change direction
- Push-based state updates — position and moving state update in real time as status frames arrive
- All automation flags as entity attributes — visible on the entity detail card and usable in automations:
dawnAutomatic,duskAutomatic,sunAutomatic,timeAutomatic,manualMode,sunMode,ventilatingMode,ventilatingPosition,sunPosition,windAutomatic,rainAutomatic,windMode,rainMode,windDirection,rainDirection,blindsMode,slatPosition,slatRunTime,tiltInSunPos,tiltInVentPos,reversal,motorDeadTime,runningTime, and more — depending on device type and status format - Obstacle / Block detection — the Rohrmotor (
0x49) and SX5 (0x4E) get dedicatedobstacleandblockbinary sensor entities, usable directly as State triggers in automations. The SX5 additionally gets alight_curtainentity. Other cover types may support this too but are unverified — open an issue if your device reports obstacle/block in FHEM. No real frames available yet. - SX5 Light Curtain — the SX5 garage door (0x4E) additionally gets a
light_curtainbinary sensor entity - Firmware version — shown in device info after first status frame
- Battery state — shown as attribute where applicable
- On / Off — standard switch commands
- Universalaktor (0x43) — creates two separate switch entities (one per channel: 01 and 02), both grouped under the same device in HA
- All automation flags as attributes —
dawnAutomatic,duskAutomatic,sunAutomatic,timeAutomatic,manualMode,sunMode,stairwellFunction,stairwellTime,modeChange
- On / Off — full on / full off
- Brightness control — HA brightness (0–255) mapped to DuoFern level (0–100)
- All automation flags as attributes —
dawnAutomatic,duskAutomatic,sunAutomatic,timeAutomatic,manualMode,sunMode,stairwellFunction,stairwellTime,intermediateMode,intermediateValue,saveIntermediateOnStop,runningTime
- Target temperature — set desired temperature (4.0–28.0 °C for HSA and 4.0–40.0 °C for Raumthermostat in 0.5 °C steps)
- Current temperature — measured temperature from the device
- HVAC modes — HEAT and OFF
- All readings as attributes —
temperatureThreshold1–4,actTempLimit,output,manualMode,timeAutomatic; for the Heizkörperantrieb additionally:sendingInterval - Manual Mode / Time Automatic switches —
manualModeandtimeAutomaticare exposed as configuration switch entities for both the Raumthermostat (0x73) and Heizkörperantrieb (0xE1), matching the same switch pattern used for cover and switch devices - Temperature zone buttons — four buttons ("Activate Zone 1–4") on the Raumthermostat (0x73)
device card activate one of the four stored temperature threshold zones. Usable in automations
via
button.press. The threshold values are configurable via the four number sliders (temperatureThreshold1–4, 4.0–40.0 °C) - Valve Position sensor — dedicated sensor entity (0–100 %) for the Heizkörperantrieb (
0xE1), visible on the device card - Battery sensor — dedicated diagnostic sensor entity for the Heizkörperantrieb (
0xE1), readsbatteryPercentfrom the status frame and persists the last known value across restarts - Window Open Signal switch — tells the Heizkörperantrieb a window is open, immediately forcing the valve to the setback temperature (4 °C). The switch reflects the live device state — the device echoes the last-set value back in every status frame
- Boost Mode — rapidly heats a room by fully opening the valve for a configurable duration:
- Boost switch — activates / deactivates boost mode
- Boost Duration number (4–60 min) — configure the duration before activating; moving the slider alone sends nothing to the device
- Boost Started sensor (timestamp) — shows when the last boost was activated, rendered by HA as "13 minutes ago"; persists across restarts
- Values restored on startup — all
0xE1entities (climate temperatures, valve position, sending interval, boost duration) show their last known values immediately after HA restarts. Battery devices can take several minutes before their first status frame — no moreunknownon the device card
- Bewegungsmelder (0x65) —
motiondevice class, state updated viaduofern_event - Rauchmelder (0xAB) —
smokedevice class, state updated viaduofern_event; battery level is persisted across HA restarts - Fenster-Tür-Kontakt (0xAC) —
openingdevice class; two entities per device:openedandtilted - Battery sensor — battery-powered sensors (Bewegungsmelder
0x65, Rauchmelder0xAB, Fenster-Tür-Kontakt0xAC) get a dedicated Battery diagnostic sensor entity (0–100 %) visible on the device card. The last known value persists across HA restarts.battery_state(ok/low) is exposed as an attribute on the battery entity
Covers with obstacle detection hardware get two dedicated binary sensor entities each:
| Entity | Device Class | Triggered when |
|---|---|---|
| Obstacle | problem |
Device detected an obstacle during movement |
| Block | problem |
Device is blocked and cannot move |
The SX5 garage door (0x4E) additionally gets:
| Entity | Device Class | Triggered when |
|---|---|---|
| Light Curtain | safety |
The safety light curtain is active |
Devices with confirmed obstacle detection: Rohrmotor (0x49), SX5 (0x4E). Other cover types (Rohrmotor-Aktor 0x42, Connect-Aktor 0x4B, Troll Basis 0x4C, Troll Comfort 0x70) may support obstacle/block but are unverified — open an issue if your device reports these in FHEM.
These entities are fully triggerable in HA automations as State triggers — see the Automations section.
Environmental sensor devices expose one or two binary sensor entities depending on their capabilities:
| Device | Code | Sun sensor | Wind sensor |
|---|---|---|---|
| RolloTron Comfort Master (built-in) | 0x61 |
✅ | — |
| Sonnensensor | 0xA5 / 0xAF |
✅ | — |
| Sonnen-/Windsensor | 0xA9 |
✅ | ✅ |
| Markisenwaechter | 0xAA |
— | ✅ |
Sun and wind sensor states are preserved across HA restarts via RestoreEntity.
One sensor entity per measurement:
| Sensor | Unit | Device Class |
|---|---|---|
| Brightness (Helligkeit) | lux | illuminance |
| Temperature (Temperatur) | °C | temperature |
| Wind Speed | m/s | wind_speed |
| Sun Direction (Sonnenrichtung) | ° | — |
| Sun Height (Sonnenhöhe) | ° | — |
These buttons appear on the DuoFern Stick device card:
| Button | What it does |
|---|---|
| Start pairing | Opens a 60-second pairing window. Press the pair button on a new DuoFern device to add it. The device is auto-added to the config on success. |
| Start unpairing | Opens a 60-second unpairing window. Press the unpair button on a paired device to remove it. The device is auto-removed from the config on success. |
| Stop Pairing/Unpairing | Stops the active pairing or unpairing window early. Only available when a window is open. |
| Status Broadcast | Sends a broadcast status request to all paired devices, refreshing all states in HA. |
Pair DuoFern devices by entering their 6-digit device code — no physical button press required. This replicates the Rademacher Homepilot "Code anmelden" functionality.
How to use:
- Put the device in pairing mode (within 2 hours of power-on, or set to RemotePair)
- Enter the 6-digit hex code (printed on the device) in the "Pair by Code" text field on the stick device card
- Press the "Pair by Code" button
- If successful, the device is added automatically and the integration reloads
Only 6-digit device codes are supported. 10-digit (2020+) devices must be paired via Homepilot first, then added via Auto-Discovery or paired using button press method.
| Button | Devices | What it does |
|---|---|---|
| Dusk position | All covers | Move to stored dusk position |
| Dawn position | All covers | Move to stored dawn position |
| Toggle | All covers | Reverse current movement / change direction |
| Reset settings | Covers, switches, dimmers, climate | Reset device settings (keeps pairing) |
| Full reset | Covers, switches, dimmers, climate | Factory reset (loses pairing) |
| Remote pair | All actuators | Initiate remote pairing |
| Remote unpair | All actuators | Remove remote pairing |
| Stop remote pairing | All actuators | End remote pair/unpair window early |
| Get status | All actuators | Request current status from this device |
| Activate Zone 1–4 | Raumthermostat (0x73) | Activate one of the four temperature threshold zones (actTempLimit) |
Each paired Handsender or Wandtaster gets a dedicated EventEntity in HA. When a button is pressed, the entity fires with the action (up, stop, down, stepUp, stepDown, pressed, on, off) and channel number, making it directly usable in automations via the Device trigger UI — no YAML required.
- Push-based, no polling — devices push status updates; HA reflects changes immediately
- Status broadcast on startup — on integration load, a full status broadcast ensures all device states are current
- USB auto-discovery — the stick is detected automatically via USB VID/PID when plugged in
- Battery sensor entity — all battery-powered devices get a dedicated Battery diagnostic sensor entity on the device card. The last known value persists across HA restarts
- Last Seen sensor — every device gets a
Last Seentimestamp sensor that updates whenever a frame is received, withRestoreEntitypersistence - Automatic device discovery (opt-in) — unknown devices that send frames but are not yet in your paired list automatically appear in the HA Discovered inbox. Enable under Settings → Devices & Services → DuoFern → Configure. See Automatic Device Discovery below
- Auto-add on pairing — when a new device is learned via the stick's pairing button, its hex code is automatically written into the config and the integration reloads. No more digging through logs
- Auto-remove on unpairing — when a device is unpaired during an active unpairing window, it is automatically removed from the config and the integration reloads
- Pair by Code — pair devices by entering their 6-digit code directly in the UI, no button press on the device required. Replicates the Homepilot "Code anmelden" functionality
- Open HACS in Home Assistant
- Click the three-dots menu → Custom repositories
- Add
https://github.com/irstmon/homeassistant-duofernwith category Integration - Search for "Rademacher DuoFern" and install
- Restart Home Assistant
Copy the custom_components/duofern/ folder to your HA config directory:
/config/custom_components/duofern/
Then restart Home Assistant.
Go to Settings → Devices & Services → Add Integration → DuoFern
- Serial Port — select your DuoFern USB stick (e.g.,
/dev/ttyUSB0) - System Code — the 6-digit hex dongle serial (starts with
6F, e.g.,6F1A2B). Find it in your previous FHEM config (ATTR dongle CODE) or on the stick label. To preserve all existing pairings you need to use the same code as before! Otherwise all devices have to be re-paired
Enter the 6-digit hex codes of your paired DuoFern devices, separated by commas:
406B2D, 4090AE, 40B690, 436C1A
These are the device codes from your FHEM configuration (ATTR device CODE).
Go to Settings → Devices & Services → DuoFern → Configure to add or remove device codes at any time. The integration reloads automatically.
If you enable "Automatically discover unknown devices" in the options, any DuoFern device that sends a frame but is not yet in your paired list will automatically appear in Settings → Devices & Services → Discovered:
- The device is only shown if its type is recognized (known Rademacher device — not radio noise)
- Click Add to add it to your paired list and reload the integration
- Click Ignore to permanently suppress it — HA handles this natively and it will never reappear
This is useful if you forgot to add a device code during setup, or want to discover the hex code of a device without looking it up in FHEM.
- Note your system code and all device codes from FHEM (
list TYPE=DUOFERN) - Install this integration and enter the same codes during setup
- Device pairing is stored in the DuoFern devices themselves and tied to the system code — as long as you use the same system code during setup, all previously paired devices will respond without re-pairing. No re-pairing needed
- All device states are refreshed automatically via the startup status broadcast
trigger:
- platform: state
entity_id: binary_sensor.duofern_rohrmotor_xxxxxx_obstacle
to: "on"
action:
- service: cover.open_cover
target:
entity_id: cover.duofern_rohrmotor_xxxxxx
- service: notify.notify
data:
message: "Obstacle detected — shutter re-opened."trigger:
- platform: event
event_type: duofern_event
event_data:
device_code: "A0XXXX"
event: "up"
channel: "01"Or use the Device trigger UI in the automation editor — no YAML needed.
condition:
- condition: template
value_template: >
{{ state_attr('cover.rollotron_living_room', 'sunAutomatic') == 'on' }}The tools/ directory contains standalone Python scripts for testing and device management without Home Assistant.
Important Note: The HA integration must be stopped while using CLI tools — only one process can hold the serial port at a time.
pip install pyserial pyserial-asyncio-fastHAOS (Home Assistant OS) Note: On HAOS the system Python is externally managed. You need to run:
apk add py3-pip pip install --break-system-packages pyserial-asyncio-fastThis is only needed for the CLI tools. The integration itself installs dependencies automatically via
manifest.json.
Control roller shutters directly from the command line:
python3 tools/test_duofern.py 4053B8 up # Open one shutter
python3 tools/test_duofern.py 4053B8 down # Close one shutter
python3 tools/test_duofern.py 4053B8 stop # Stop one shutter
python3 tools/test_duofern.py 4053B8 position 50 # Set one to 50%
python3 tools/test_duofern.py 4053B8 status # Status of one device
python3 tools/test_duofern.py up # Open ALL shutters
python3 tools/test_duofern.py down # Close ALL shutters
python3 tools/test_duofern.py position 50 # Set ALL to 50%
python3 tools/test_duofern.py status # Status of ALL devices
python3 tools/test_duofern.py statusall # Broadcast status requestPair and unpair DuoFern devices without FHEM:
python3 tools/pair_duofern.py pair # Start pairing (60s window)
python3 tools/pair_duofern.py unpair # Start unpairing
python3 tools/pair_duofern.py list # List all devices with status
python3 tools/pair_duofern.py pair --timeout 120 -v # Extended timeout + debug- Frame format: Fixed 22-byte (44 hex char) frames over UART at 115200 baud
- Init sequence: 7-step handshake (Init1 → Init2 → SetDongle → Init3 → SetPairs → InitEnd → StatusBroadcast)
- ACK-gated send queue: One command in-flight at a time, 5-second timeout
- Push-based updates: Devices send status frames proactively; coordinator calls
async_set_updated_data()on each received frame - Position convention: DuoFern 0 = open / 100 = closed; HA 0 = closed / 100 = open (inverted transparently)
- HSA (Heizkörperantrieb): Device-initiated bidirectional protocol — changes are queued and transmitted only when the device checks in with a status frame, matching FHEM's
%commandsHSA/HSAoldimplementation - Boost frame layout (OTA-verified via rtl_433):
- ON:
f[8] = 0x40 | duration_min(only if duration changed, else0x00),f[11] = 0x03;svcontains desired-temp only if it was changed, else0x000000 - OFF:
f[8] = 0x00,f[11] = 0x02(critical —0x00is silently ignored by the device)
- ON:
- Code-Pairing protocol (OTA-verified via rtl_433):
- USB frame byte 21 (flags) controls
pay[0]in the radio frame:0x00= normal command,0x01= pairing mode - Sequence: SetPairs (0x03) → StartPair (0x04) → RemotePair ×2 (0x0D, flags=0x01) → wait for 0x06 response → StopPair (0x05)
- The stick must be in pairing mode (StartPair) before sending the pair frame
f[1]=0xFFrequired for correct radio payload mapping (pay[7]=FF)
- USB frame byte 21 (flags) controls
To capture raw OTA frames with an RTL-SDR dongle (thanks a lot to gluap from pyduofern-hacs for writing down his command and pointing me to it):
rtl_433 -s 2.0M -f 434.5M -g 30 \
-X "n=duofern,m=FSK_MC_ZEROBIT,s=10,r=100,preamble={10}fd4,invert" \
-S knownImplementation based on 10_DUOFERNSTICK.pm and 30_DUOFERN.pm from the FHEM project.
Yes, this project does make use of LLMs and coding agents, and this will likely continue going forward. AI is integrated deliberately and with care.
If you use AI tools, please do so responsibly and transparently.
On a personal note: the use of AI tools doesn’t mean this project was quick or effortless to build. A lot of time and dedication went into it.
MIT License — see LICENSE for details.