|
Home IO Control
ESPHome add-on for IO-Homecontrol devices
|
IO-Homecontrol covers a wide range of motors and actuators, and every model can behave slightly differently during pairing — different discovery commands, timings, or radio settings. Nobody can test against every device in the field, so getting pairing to work reliably on a specific device sometimes needs per-device fine-tuning.
The tuning: block exists for exactly that. It exposes the pairing and radio parameters that are normally fixed, so you can experiment on a stubborn device and find a working combination — then copy that combination back into your configuration.
Add a tuning: block under home_io_control: and set ui_controls: true:
home_io_control: node_id: "C0FFEE" system_key: "..." tuning: ui_controls: true
ui_controls: true exposes every parameter as a Home Assistant number or select entity, so you can change values live from the UI without reflashing for each experiment. The entities appear under the device's Configuration section, grouped into Radio … and Pairing … names.
Turn on verbose logging. Set the logger to DEBUG so you can see the pairing exchange and the raw packet capture:
At boot and at the start of every pairing attempt, the component logs the active overrides:
or, when nothing is overridden:
Each UI change is also logged in YAML-compatible form, ready to paste back:
Parameters fall into two groups: the radio / physical layer (how the chip transmits and receives) and the pairing protocol (which frames are sent and how the hub waits for answers). The Radio column shows which chip a parameter affects — a chip-specific parameter (SX1262, SX1276, or LR1121) is ignored on any other chip's boards; both applies everywhere.
Each parameter below has a short What we've seen note. These summarise concrete behaviour observed while getting pairing to work — treat them as starting hints, not guarantees, because your device may differ.
| Parameter | Radio | Default | Range / options | What it does |
|---|---|---|---|---|
| sx1262_rx_bandwidth | SX1262 | 58.6 | 39.0 / 46.9 / 58.6 / 78.2 / 117.3 / 156.2 / 187.2 (kHz) | Receiver bandwidth (double-sideband). Widen it when one device's replies are missed. |
| sx1262_response_preamble | SX1262 | 12 | 8–256 B | Preamble length on reply frames, for the peer to lock on. |
| sx1262_post_tx_settle_us | SX1262 | 500 | 0–2000 µs | Settling delay after TX before switching back to RX. |
| sx1276_rx_bandwidth | SX1276 | 41.7 | 20.8 / 41.7 / 62.5 / 83.3 / 125.0 (kHz) | Receiver bandwidth (single-sideband, so 41.7 spans 83.4 kHz); wider tolerates LO offset, narrower rejects more noise. |
| sx1276_response_preamble | SX1276 | 12 | 8–256 B | Preamble length on reply frames, for the peer to lock on. |
| sx1276_discovery_hop_slice_ms | SX1276 | 5 | 5–200 ms | Per-channel dwell for any hopping listen — discovery and the scan_paired_devices roll-call alike. |
| sx1262_discovery_hop_slice_ms | SX1262 | 7 | 0–500 ms | Per-channel dwell for any hopping listen — discovery and the scan_paired_devices roll-call alike. |
| exchange_start_response_wait_ms | both | 400 | 200–4000 ms | How long to listen for a reply to a start frame (the first frame of a command). |
| exchange_early_response_wait_ms | both | 200 | 150–1000 ms | How long every attempt but the last listens when a mains device is asked more than once. The last attempt listens exchange_start_response_wait_ms. |
| exchange_response_wait_ms | both | 500 | 200–4000 ms | How long to listen for a reply to a continuation frame, and for the post-auth final response. |
| exchange_total_budget_ms | both | 2500 | 500–12000 ms | Wall-clock ceiling on one whole exchange, including retries. |
| lr1121_rx_bandwidth | LR1121 | 117.3 | 39.0 / 46.9 / 58.6 / 78.2 / 117.3 / 156.2 / 187.2 (kHz) | Receiver bandwidth (double-sideband). 117.3 is validated on real LR1121 hardware. |
| lr1121_response_preamble | LR1121 | 12 | 8–256 B | Preamble length on reply frames, for the peer to lock on. |
| lr1121_post_tx_settle_us | LR1121 | 500 | 0–2000 µs | Settling delay after TX before switching back to RX. |
| lr1121_discovery_hop_slice_ms | LR1121 | 7 | 0–500 ms | Per-channel dwell for any hopping listen — discovery and the scan_paired_devices roll-call alike. |
| cold_broadcast_reply_preamble | both | 80 | 8–256 B | Preamble length for the key-extraction responder's discovery reply (0x29) — the one reply a hopping peer has to catch cold. |
| key_extraction_type_word | both | 0x0080 | 0x0000–0xFFFF | Device type the key-extraction responder's discovery reply (0x29) claims, as the reply's two type bytes. Applies to an open search only. See below. |
| key_extraction_multi_info | both | 0xDD | 0x00–0xFF | The key-extraction responder's discovery reply (0x29) flags byte: reply speed and whether the device sleeps. See below. |
| normal_start_preamble | both | 48 on SX1262/LR1121, 32 on SX1276 | 8–256 B | Preamble length for a directed start frame to a device not declared low_power: — an always-alive receiver that does not need the 1024-byte wake-up burst. Low-power devices lead with LONG_PREAMBLE unless `low_power_wake_belief` believes them awake. Also governs a 1W oneway_controllers: identity's non-wake-up copies when its own low_power: is false or true (unset keeps 1W on LONG_PREAMBLE — see Sending 1W commands). |
| low_power_wake_preamble | both | 1024 | 1024–4096 B | Wake-up preamble on a directed start frame to a low_power: device believed at rest. See below. |
| low_power_wake_belief | both | true | true / false | Diagnostic switch. On, a low_power: device that was recently moving or heard from gets the short start preamble on its first try; off, every try uses the 1024-byte wake-up preamble. See below. |
| lbt_max_retries | both | 5 | 0–10 | Listen-before-talk checks on the transmit channel before the first frame of an exchange is sent without one. |
| lbt_rssi_threshold_dbm | both | -90 | -95 to -70 dBm | RSSI on the transmit channel below which it counts as free. |
| remote_activity_poll_delay_ms | both | 2000 | 0–120000 ms | Delay from a linked-remote press (not a STOP) to the first status poll of its devices. See below. |
| remote_activity_catchup_poll_ms | both | 0 (off) | 0–600000 ms | Read every device with linked_remotes this long after the last exchange that followed a linked-remote press, to catch a press the hub missed. See below. |
| pairing_discovery_commands | both | ["0x28"] | ordered list of 0x28 / 0x2E | Which discovery command(s) to send, and in what order. |
| pairing_discovery_destination | both | auto | auto / 0x00003B / 0x00003F / 0x0001BB / 0x0001BF / 0x00FFFB | Address the discovery frames are sent to. The last three are device-class addresses — see below. |
| pairing_discovery_payload | both | none | none / 0x00 | Optional payload byte (used by the alternate command). |
| pairing_discovery_low_power | both | false | true / false | Sets the LOW_POWER flag in discovery frames. |
| pairing_discovery_ack_capable | both | false | true / false | Sets the ACK (CTRL1_ACK) flag on the discovery broadcast only. Off by default — see note below. |
| pairing_discovery_wait_ms | both | 2000 | 500–5000 ms | How long to wait for a response after each discovery TX. Also the per-attempt listen window for each of the scan_paired_devices roll-call's attempts (up to six: three per power class). |
| pairing_discovery_initial_dwell_ms | both | 300 | 0–500 ms | Settle delay before the first discovery TX. |
| pairing_key_exchange_retries | both | 3 | 1–5 | Retries for the authenticated key-exchange phase. |
| scan_power_classes | both | both | both / always_alive / low_power | Which device power classes the scan_paired_devices roll-call calls. |
| pairing_discover_confirm | both | send | skip / send / send_with_ack | Whether/how to send DISCOVER_CONFIRM (0x2C) to a freshly-discovered device before the key exchange. |
| pairing_key_init_delay_ms | both | 300 | 0–10000 ms | Pause after the discover-confirm step (ACKed, refused, or silent) and before KEY_INIT (0x31). |
| pairing_discovery_listen_channels | both | skip_request | skip_request / all | Which channels the wait for a discovery response covers. |
| pairing_key_transfer | both | auto | auto / push / pull | Send the hub's key to the device, or ask the device for its own key. auto pulls from a device that answers discovery as a remote controller and pushes to any other. |
ui_controls itself is a feature toggle (default false) that exposes these as entities; it is not a tunable.
GFSK receiver bandwidth on the SX1262. Change it when frames arrive but fail to decode — the did not parse as a frame warnings in the log are a direct count of that.
The values are double-sideband: the total width of the filter, centred on the hub's own frequency. Semtech sizes a GFSK filter as bitrate + 2 × deviation + carrier offset, which is 76.8 kHz for this waveform before any offset. The SX1262 has no automatic frequency correction in this mode, so the filter alone has to absorb how far a device's transmitter sits from nominal.
Observations: 58.6 kHz is the default. It is below the 76.8 kHz figure, so it trims the edges of even an on-frequency signal, but it made a Somfy RS100 solar shutter respond reliably where 117.3 did not. A device whose transmitter sits off nominal needs the opposite: a Somfy LightVar_Wh_io dimmer went from mostly missed replies to 9 of 11 commands confirmed at 156.2, with 117.3 and 187.2 both worse in the same test (issue #119). The setting applies to every device, so pick the value your least cooperative device needs and check that the others still answer.
If one device's replies are missed — wait_first_timeout, or a challenge that arrives without its final response — while other devices are fine, try 117.3 and 156.2. 39.0 and 46.9 are well below the 76.8 kHz figure; use them only to probe a noisy install.
How long a preamble the SX1262 puts in front of its reply frames (the key-transfer and authentication responses), giving the peer time to lock on after the hub transmits. Raise it when key exchange stalls right after discovery on an SX1262 board, especially with a device that has never been paired before.
Observations: the SX1262's modulation is marginal for short preambles on the fast post-TX turnaround. The default of 12 bytes puts about the same preamble on air as a real controller's replies (about 2.6 ms) and matches the SX1276 default. In one field run a Somfy light's closing reply was lost after 13 of 38 challenges with 8 bytes, against 4 of 35 with 12 (issue #119). 8 bytes is the protocol's nominal floor; already-paired devices lock onto it, but brand-new devices often fail to decode on-air preambles of 1/4/8 bytes and need something substantially longer. How much longer, up to the protocol's full 1024-byte long preamble, has not yet been independently validated on this chip, so treat any specific number above 12 as untested rather than known-good. A longer preamble also delays the reply, and some devices reject a late one (see troubleshooting). The same "not below the tens of bytes" caution is why normal_start_preamble (below) defaults to 32 rather than 8.
How long the SX1262 waits after transmitting before switching back to receive. Increase it when fast replies (the challenge or key frames) arrive corrupted.
Observations: experiments here established ~500 µs as the minimum that keeps the demodulator from mangling the first bytes of a quick reply after the transmit→standby→receive transition. It works hand-in-hand with bandwidth — a narrower bandwidth generally wants a longer settle.
How long the hub listens for a device's reply before giving up on a try. Raise exchange_start_response_wait_ms when a device ignores commands but is known to be in range and correctly paired.
Observations: for this device class, reply latency is fast-or-never rather than variably slow — a device answers within a few milliseconds of the carrier dropping, or it does not answer at all. A failure therefore shows up as no frame received rather than as a late arrival, so raising this value cannot fix a device that genuinely fails to respond; check wait_ms in the logs first. The 400 ms default sits comfortably above every directly measured reply from this device class while keeping a failed exchange inside exchange_total_budget_ms. Raise it only for a device you have confirmed genuinely answers late.
Every millisecond here is loop-blocking time on a failed exchange only (a successful one returns as soon as the reply lands, see ADR 0013), and a failure costs this window once per retry (once per request for a mains device whose earlier attempts listen `exchange_early_response_wait_ms`). Raise it for a stubborn device; lower it if slow failures are worse for you than missed commands.
A mains-powered device answers within tens of milliseconds or not at all, so a command or a status read that gets no answer does not need to wait the whole exchange_start_response_wait_ms before it is sent again. The hub sends such a request again without a gap: every attempt but the last listens exchange_early_response_wait_ms, and the last listens the whole start window, so a device that answers late still gets one full-length listen. With the defaults that is up to nine attempts about 220 ms apart, ending about 2.2 s after the first if none is answered. A low_power device keeps one window on every attempt, and a stored-position command (favourite, vent) is sent as before, three attempts with a gap, because a second copy can stop what the first started (ADR 0075).
Observations: of the 52 start requests in the project's corpus that went to a device not declared low_power, 47 were answered within 98 ms (median 66 ms). On the bench, an Izymo dimmer answered 36 to 42 ms after the start of the hub's transmission and a Sunea awning answered its status reads within 33 to 49 ms. 200 is twice the slowest of the corpus replies. Raise it for a mains device whose wait_ms in the Try N answered log line exceeds it; fewer attempts fit the budget the higher it goes, and a value above exchange_start_response_wait_ms has the same effect as that value.
Wall-clock ceiling on one whole exchange, including retries. exchange_start_response_wait_ms and exchange_response_wait_ms set how long each try waits; this caps how long all of them together may run, so a try only starts if it can finish inside the budget. The time already spent, the gap before the try, the try's own transmission (about 220 ms with a 1024-byte wake-up preamble, ADR 0044), the window the try then listens (about 10 ms of channel check and hop around each transmit are counted too), and, when the device has already challenged in this exchange, the wait for its final reply and the answer to its challenge must together stay below it (ADR 0073, ADR 0075). That last part matters for a low-power device that challenges: two tries use the default budget, so a third is not started.
The budget also sets how often a status read that decides where a cover ended up is repeated: the poll after a stop, the poll that confirms a cover that stopped short of its target, the polls about 5, 15 and 30 seconds after a miss, and a one-off or refresh read. Such a read repeats without a gap, as many times as fit (nine for a mains-powered cover at the default budget, never more than nine; three for a low_power device, plus one short attempt when the hub has no recent news of it or the read is a catch-up read, see `low_power_wake_belief`), the early attempts listening `exchange_early_response_wait_ms`. A command that is harmless to repeat (a position, open, close, stop) is sent again the same way to a mains-powered device. If every attempt is lost, the request blocks the hub for up to about 2.2 s, and a stop pressed meanwhile waits behind a read. The reads during a move stay single tries, so a stop pressed mid-move is not held up. See ADR 0074.
Observations: this exists to keep a failing command from blocking the ESPHome loop past its own "took a long time for an operation" warning threshold (2550 ms — see ADR 0013). If you raise either response-wait parameter, raise this too, or later retries within the same command will silently be skipped once the budget runs out.
GFSK receiver bandwidth on the SX1276, written to both the RX and AFC bandwidth registers. Change it when discovery or key-exchange replies fail to decode cleanly on an SX1276 board.
Observations: the values are single-sideband — half the filter's total width — so the default 41.7 kHz spans 83.4 kHz, just above the 76.8 kHz Semtech sizing figure for this waveform, and is validated against real devices. The SX1276 also re-centres on each packet's carrier automatically, which absorbs a device's frequency offset that the SX1262 cannot. It has worked reliably at this default across the devices tested here, so this knob is exposed for marginal-range or drifting installs rather than because a change was needed. Widen it (62.5/83.3/125.0) when a device's transmitter drifts more than the controller's radio, at the cost of admitting more noise; narrow to 20.8 for maximum noise rejection on a clean signal.
How long a preamble the SX1276 puts in front of its reply frames (the key-transfer and authentication responses), giving the peer time to lock on after the hub transmits. Raise it if key exchange stalls right after discovery on an SX1276 board.
Observations: the default here is 12 bytes — longer than the protocol's 8-byte short preamble, which on hardware measurably improves the peer's lock-on at no timing cost. It is the same as the SX1262 default; it has never needed raising in testing here. Lengthen it further for a stubborn or marginal-range device.
How long the receiver dwells on each channel while hopping. This governs every rotating listen in the project, not just discovery: pairing discovery and the scan_paired_devices broadcast roll-call both rotate across channels and both fall back to this value when they have no loop-specific reason to dwell differently (neither does today). Change it when a device is clearly present but its discovery response, or its roll-call reply, is never caught.
Observations: because a device often answers on a different channel than the request was sent on, hopping during the wait is essential — a hop slice that never rotates would miss it. Both chips dwell only a few milliseconds per channel and extend the dwell when a preamble or sync word is detected, so a caught reply is not cut off mid-retune: the SX1276 uses a naturally fast hop cycle, and a short SX1262 dwell fits more retunes into the same window, so the receiver is more often already on the right channel when a reply starts. The floor of 0 is not a physically meaningful minimum for the chip — coverage degrades gradually below the low single digits and only collapses at the literal 0, where wait_for_packet(..., 0) returns before anything can be observed at all — so it is left open as an experimentally checkable floor rather than assumed. Values near that floor are for probing it, not for everyday tuning.
The LR1121 equivalents of the four SX1262 knobs above — same meaning (lr1121_discovery_hop_slice_ms governs the roll-call as well as discovery, same as its SX1262/SX1276 counterparts), same register-level reasoning (the LR1121's GFSK bandwidth encoding is register-identical to the SX1262's, and it needs the same standby→retune→RX hop cycle, no fast hop). Three of the four share SX1262's default values (lr1121_response_preamble, lr1121_post_tx_settle_us, lr1121_discovery_hop_slice_ms); lr1121_rx_bandwidth instead keeps its own wider default, 117.3 kHz, which clears the 76.8 kHz sizing figure and is validated on this chip. lr1121_discovery_hop_slice_ms is measured independently on LR1121 rather than merely inherited, since the two chips are validated separately and could in principle diverge.
Observations: the defaults were seeded from SX1262's validated values and are confirmed working on real LR1121 hardware — authenticated open/close/stop exchanges complete reliably against a real awning at the stock settings. The main variable that still matters in practice is RF link quality (RSSI) rather than these timing/bandwidth knobs — weak signal shows up as intermittent frame loss on either leg of an exchange, which the existing per-command retry already absorbs.
Sized independently of response_preamble(): it governs only the "Recover System Key" feature's discovery reply (0x29), the sole device-role reply that is start=true and therefore the one a hopping/scanning peer has to catch cold, rather than a peer already parked on a held channel.
These two set the bytes the "Recover System Key" responder uses to describe its pretend device in its discovery reply (0x29). Change them when your hub keeps searching but never takes the device: some hubs only list products they know, or skip a device that says it sleeps.
Values that real devices send:
| Device | key_extraction_type_word | key_extraction_multi_info |
|---|---|---|
| Somfy Izymo dimmer (lighting receiver) | 0x0180 | 0xDC |
| Somfy Sunea io awning | 0x0400 | 0xDC |
| Somfy RS100 io roller shutter | 0x0080 | 0x1C |
To copy another device, take the bytes from its own discovery reply in a frame log: in 29 01 80 <address> 02 DC 00 0E, the type word is 01 80 and the flags byte is DC.
A VELUX KLR 200, a VELUX KLF 200 and a Somfy TaHoma hand over their key with the defaults. A Freebox Delta confirms the reply only with the always-listening byte 0xDC (#215). 0x1C promises an answer within 5 ms, which the responder cannot always keep, so use it only to test. If you change either value, please report the result either way.
Write the values in hex: 0xDC, or quoted as "DC" or "01 80" (quoted text is always read as hex). A bare number without 0x, such as 10, is read as decimal (0x0A), the way the Home Assistant entity shows it. With ui_controls: true, the two Home Assistant number entities show the same values in decimal (0xDC is 220, 0x0180 is 384), so you can try another value without reflashing.
home_io_control: tuning: key_extraction_type_word: 0x0180 key_extraction_multi_info: 0xDC
The preamble in front of a directed start frame (EXECUTE, status poll, GET_NAME, rename, identify, probe) whose target is not declared low_power:. An always-listening receiver does not need the ~213 ms 1024-byte wake-up burst, and some receivers never lock onto one that long — so a normal start frame gets this shorter preamble, matching what real hubs send to an always-alive device. A device declared low_power: true keeps LONG_PREAMBLE on its start frames unless it is believed awake — see `low_power_wake_belief`.
The default depends on your radio. An SX1262 or LR1121 board starts at 48 bytes, an SX1276 board at 32 (ADR 0042). The software PHY those two chips share puts less usable preamble on air than its programmed length suggests: measured against a Somfy dimmer, an SX1262 was answered on the first try 79% of the time at 32 bytes and every time at 48, while an SX1276 sending the same programmed 32 was answered every time. The config dump in your logs prints the value it resolved and whether it came from YAML or the radio.
Setting this key yourself always wins, including a value below the default — useful if you are bisecting a reception problem and want to see where your own device stops answering.
The same value governs a 1W identity's non-wake-up copies once its own low_power: is set to false or true — see Sending 1W commands. Left unset, an identity keeps LONG_PREAMBLE on every copy regardless of this setting.
Sized independently of response_preamble() (that knob is a per-chip TX→RX turnaround property; this is a cold-peer property). The 32-byte default is 256 bits, well inside the range real hubs use; drop it toward 8 only if a start frame is still not being heard and raise it toward LONG_PREAMBLE if a marginal always-alive link needs more. Because it is a live tuning knob, bisecting the right value needs no rebuild.
The wake-up preamble, in bytes, on a directed start frame to a low_power: device believed at rest. The default is 1024 (~213 ms on air). A resting VELUX SSL solar roller shutter answered it on only about half the tries, which suggests it listens less often than every 213 ms; a longer preamble spans more of its sleep cycle. Raise it in steps (1536, 2048) and compare how often a try at rest is answered (Try N answered … preamble=).
A longer preamble makes every try at rest longer: at 2048 bytes a try takes about 1.1 s, so only two fit in the default `exchange_total_budget_ms`. Raise that to about 3500 ms to keep three, and expect ESPHome to warn that the component took a long time. Pairing, discovery, the roll-call and 1W transmits keep their own preambles.
A diagnostic switch, on by default. If a device declared low_power: true got worse after updating, set this to false and report it.
A duty-cycled receiver at rest needs the wake-up preamble. One that is travelling — a VELUX solar roller shutter mid-travel, for example — ignores that long preamble and answers only the short one, so a stop or status poll sent to a moving shutter with the long preamble goes unanswered. With this on, the hub tracks per device whether it is moving, and orders the tries of each directed exchange accordingly:
| Believed | Try 1 | Try 2 | Try 3 |
|---|---|---|---|
| moving (a stop, or a move the device accepted in the last 2 minutes that is not yet known to have ended) | short | wake-up | wake-up |
| at rest | wake-up | wake-up | wake-up |
A read that decides where a cover ended up (described below), sent to a low_power cover that has sent no status for 2 minutes and is not believed moving, cannot tell which of the two it will meet: after a reboot, a refresh read or a read after overheard traffic, the cover may be travelling or at rest. Neither can the catch-up read after linked-remote activity (`remote_activity_catchup_poll_ms`), even when the cover reported a moment ago, because a press the hub missed may have set it moving. Such a read makes one extra attempt first, with the short preamble and a 100 ms listen, and then the three wake-up attempts, so it reaches a moving cover and a resting one alike. A silent short attempt changes nothing the hub believes. At the default timing the four attempts fit `exchange_total_budget_ms`; with a tighter one the read may run past it by the cost of the short attempt, about 0.12 s, and no more (ADR 0079). Experimental: no bench run has met such a read yet.
"Short" is `normal_start_preamble`, "wake-up" is `low_power_wake_preamble`. Having merely heard from the device recently does not count as moving: a resting VELUX SSL ignored the short preamble even 34 ms after it had answered. A move is "known to have ended" when a status says stopped (including the reply to a stop, which can still say "moving" while the motor winds down), or when an exchange sent while the device was believed moving (other than a stop) draws no frame at all; the next exchange then leads with the wake-up preamble. An exchange that draws no frame does not end the move while the remaining travel time the device last reported has not run out: a moving receiver that missed one frame is still moving, so the next poll leads short again. The read at the predicted end of travel (described on the cover page) leads with the wake-up preamble, since the cover has come to rest by its own count. Experimental: no low-power cover that reports a remaining time has confirmed this on hardware. A status poll that only looks at a cover that is probably still moving (most scheduled polls) makes one try and sends the first try's preamble; if it misses, the next poll a few seconds later is a read that decides where the cover ended up, and repeats as often as the exchange budget allows (three times for a low_power device, up to nine for a mains-powered one). So does the poll right after an accepted stop, the poll that confirms a cover that stopped short of its target, and a one-off or refresh read. Devices not declared low_power: are unaffected, and the frame itself never changes between tries. A mains-powered VELUX receiver that is always listening rejects the wake-up preamble, so declare it low_power: false.
To see which preamble reaches a device how soon after it last answered, read the per-try log lines. Each carries preamble= (bytes) and age_ms= (time since the device was last heard; n/a when it never was, or the switch is off):
answer_us= on the challenge line is the time from receiving the device's challenge until the hub is listening again after its answer. It includes the answer's own airtime.
With the switch off, every try to a low_power: device uses the wake-up preamble.
Listen-Before-Talk: right before transmitting the first frame of an exchange, the radio measures the channel the frame is about to go out on and sends only if it is quieter than the threshold. The hub retries up to max_retries times, 5 ms apart, then transmits anyway. Frames that answer within an exchange, such as the challenge response, go out at once without the check: the device is waiting for them, and some devices give up after a single 5 ms delay (issue #178). When the idle receiver is on another channel, the radio first retunes to the transmit channel and waits about a millisecond for the reading to settle (ADR 0045). Loosen them when transmissions are delayed on a channel that only looks busy. The threshold applies to the signal level at the antenna: on a board with a front-end module the amplifier's gain is removed from the reading first (see Hardware), so the same value means the same on every board.
Observations: a device's own pairing-mode beacons made the channel read as busy (around -77 to -83 dBm), which tripped every LBT retry before each discovery transmit and slowed pairing noticeably. Raising the threshold (less sensitive) or lowering the retry count lets the hub transmit through that.
The radio cannot listen while the hub transmits, so a press that starts during one of the hub's own polls is lost (see When a press is missed). These two parameters change what the hub does after a press on a remote in a device's linked_remotes (ADR 0080). Their defaults keep the behaviour without them.
Delay from a linked-remote press to the first status poll of the devices it controls. The hub then follows the move as after any command. A STOP still polls at once. Polls after another two-way controller's traffic keep the 2-second delay.
Observations: people press a remote several times, or press two remotes one after another, in the seconds after the first press, which is when the default polls run. On a low_power cover whose full travel takes long (about 35 s on a VELUX solar screen), a delay a little longer than that travel polls once at rest instead of tracking the move. That keeps the radio listening while someone is at the remotes and needs fewer exchanges, at the cost of a later position update.
Off at 0, the default. Otherwise a linked-remote press opens a burst, and every exchange the hub runs while it is open moves its catch-up out to this many milliseconds after that exchange. When the catch-up is due, the hub reads every device with linked_remotes, by remote id or by class:, once, and the burst closes. A cover that a missed press set moving answers "moving", and the hub then follows it to rest as after any command. A read the device does not answer is not repeated until the next burst.
The hub cannot know whether a missed press set a low_power cover moving, so the catch-up read treats its state as unknown even when it reported a moment ago: it sends one short attempt, which a moving cover hears, then the wake-up attempts a resting one hears (see `low_power_wake_belief`). This part has not run on hardware yet.
Observations: it costs one read per linked device per burst, and nothing while nobody touches a remote. That suits low_power covers, where `status_refresh_interval` wakes the motor at every interval. A few seconds, 5000 to 10000, catch a cover while it still moves, so Home Assistant shows the move as it happens; a value longer than the covers' travel time reads them once at rest instead. A press the hub missed without hearing any other press before it opens no burst, so status_refresh_interval remains the answer for that (issue #209).
Which discovery command(s) the hub broadcasts, and in what order; it sends them in sequence and stops at the first device response.
0x2A (SPE roll-call) is not an option here, on purpose. It looks superficially similar — authenticated with the configured system_key, broadcast to 0x00003B — but only devices that already hold your key answer it. A device sitting in learning mode, waiting to be paired, holds no key yet and stays silent. So adding it to this list cannot help a pairing attempt succeed; it would only add replies from devices you have already paired. It is a "who is still out there" roll-call over your existing devices — a different question entirely from "who wants to pair".
If a "who is still out there" roll-call over your already-paired devices is what you actually want, that is exactly what the scan_paired_devices hub action does — see Home Assistant actions.
Observations: plain 0x28 to 0x00003B is what works. Broadcast 0x2E has drawn no response on every device this project has real hardware evidence for — a Somfy Izymo dimmer and a Velux KLR200/KUX100 pair both went unanswered (see the CMD_DISCOVER_ALT_REQ doc comment in proto_constants.h, and GitHub issue #27 where two independent reporters also tried it with no result). It only draws a response when addressed directly to an already-known node ID, which is no help for first-contact pairing where the node ID is exactly what you don't have yet. Real full-featured controllers (Tahoma) are seen broadcasting both 0x28 and 0x2E during pairing, which is why the combined preset (0x28,0x2E) still exists in the Home Assistant selector — but on the evidence gathered so far, that's the controller hedging its bets, not 0x2E actually reaching a device 0x28 couldn't. Don't reach for 0x2E as a fix for a stuck device; see A tuning plan for what to try instead.
The address the discovery frames are sent to. With auto each command uses its conventional address (above). An explicit 0x00003B / 0x00003F forces every configured command to that address — useful for deliberately sending 0x28 to the alternate address, or vice-versa.
0x0001BB and 0x0001BF are different in kind: they name the lighting device class rather than every device. An io broadcast address packs the device type into its high bits, so light (type 6) gives 0x0001BF with the all-subtypes mask and 0x0001BB with discovery's mask. Reach for one only when a light or dimmer never answers the ordinary class-less discovery, and expect nothing from it on any other device — a class-typed address is, by construction, ignored by everything outside that class. Report what happens either way; no light has yet been confirmed to answer one.
0x00FFFB is the same kind of address for the remote controller class (type 1023, discovery's mask). An Atlantic Tatou heater that receives a key searches with this address, and a Tatou that offers its key answers that search as a remote controller (issue #199). To take such a heater's key, start its key-sending function, then press Discover & Pair with this destination. The key comes by pull: pairing_key_transfer: auto already pulls from a device that answers as a remote controller, and pull forces it. No heater has yet handed its key to the hub this way.
An optional single 0x00 payload byte, and the LOW_POWER frame flag.
Observations: the alternate discovery path is conventionally sent with the 0x00 payload and LOW_POWER set, and some devices filter on them; they have no effect on the plain 0x28 path, so only enable them alongside 0x2E.
Sets CTRL1_ACK ("sender can handle 2W responses") on the discovery broadcast (0x28/0x2E) only — not on any other frame in the pairing handshake. It has no effect on the scan_paired_devices roll-call (0x2A) either, which is a separate, unrelated request path with its own fixed frame shapes: the roll-call's low-power pass sets CTRL1_ACK itself, unconditionally, as part of that pass's frame — see `scan_power_classes`.
Off by default, and scoped to the discovery broadcast on purpose. Setting CTRL1_ACK on every outbound frame silences real Somfy awnings: they drop a frame carrying an unexpected CTRL1 bit, with no error and no reply. Real VELUX hubs send their discovery broadcast with this bit set, and it is part of the confirmed pairing setup for a VELUX SSL solar roller shutter (see VELUX INTEGRA). For other families, turn it on only to test a device that never answers 0x28, and turn it back off if it doesn't help.
The preamble length (in bytes) on the discovery broadcast itself (0x28/0x2E). Unset by default: discovery then follows `normal_start_preamble`, the same length every first frame to an always-awake device gets (32 bytes on an SX1276, 48 on an SX1262 or LR1121). A short discovery is what VELUX SSL and SML shutters answer, so Discover & Pair needs no tuning entry for them. Some awake receivers never lock onto the 1024-byte wake-up burst, which is why the default is short.
Two cases change the default without setting this value. With `pairing_discovery_low_power: true` the discovery frame carries the low-power flag, and a first frame with that flag takes the wake-up preamble (low_power_wake_preamble). Changing normal_start_preamble moves discovery with it.
Setting a value overrides both, 1024 included. Use 1024 for a device that stays silent behind the short discovery and may be a duty-cycled receiver (see A tuning plan); try 8 for one that wants it shorter still:
tuning: ui_controls: true pairing_discovery_preamble: 1024 # the wake-up burst; 8 (SHORT_PREAMBLE) goes the other way
A value written through the Home Assistant number stays set until the next restart. While nothing is set, the number shows the length discovery uses at boot; discovery follows a later change of normal_start_preamble or pairing_discovery_low_power at once, the number only after a restart. The log line Tuning overrides active names this setting only when it was set.
It also bounds the rest of pairing. The frames the hub then sends directly to the discovered device (discover-confirm 0x2C, key-init 0x31, and the phase-3 SetConfig1 0x6F) never use a longer preamble than discovery did. The device has just answered a discovery with that preamble, so it is listening and hears it, and some VELUX receivers ignore every frame behind the 1024-byte wake-up burst those frames would otherwise carry (ADR 0029, ADR 0078). With a set value the log says so (Pairing: discovery preamble … B (set in tuning); directed frames to … are capped to it). Normal operation after pairing is not affected: it follows the device's own low_power setting and its wake belief (see `low_power_wake_belief`).
How long to wait for a response after each discovery transmit, and an initial settle before the first transmit. pairing_discovery_wait_ms also sets the per-attempt listen window for the scan_paired_devices action (see Scan Paired Devices); at the default scan_power_classes: both that action makes six attempts (three channels, times two power classes), so its total runtime is roughly 6x this value — 3x with `scan_power_classes` narrowed to one class.
Observations: the 300 ms initial dwell mirrors the conventional wait after a start frame. Lengthening the wait only helps when responses are intermittent — merely extending the dwell without hopping across channels did not help in testing; the fix was the hopping itself.
How many times to retry the authenticated key exchange, each attempt using a fresh challenge.
Observations: the key-transfer step is the most fragile part of the flow on SX1262 — a device may acknowledge the challenge yet never confirm the key. That is usually a preamble/turnaround problem, so if extra retries alone don't help, address sx1262_response_preamble and sx1262_post_tx_settle_us first. Retries mainly guard against occasional transient decode failures.
Which device power classes the scan_paired_devices roll-call calls (see Scan Paired Devices):
Bisecting a mixed installation in the field is the main reason to touch this: run low_power alone to confirm which devices only answer the wake-up call, then always_alive alone to confirm the rest still answer without it.
Whether — and how — the hub sends CMD_DISCOVER_CONFIRM (0x2C) directly to a freshly-discovered device before proceeding to the key exchange (0x31). Every real controller this project has captured sends this frame between the device's discovery response (0x29) and its own key-init (0x31).
The step never fails a pairing attempt in any mode: a refusal, silence, or wrong reply is logged and pairing proceeds to the key exchange regardless. Cost: about +0.3 s (the pause) plus the device's own reply latency when a 0x2D comes back on the first try; with no answer at all, up to roughly 4.8 s for an always-alive target and 5.4 s for a low-power one (three 1.5 s tries, the longer figure from the low-power target's wake-up preamble), pause included.
A pause after the discover-confirm step and before CMD_KEY_INIT (0x31), applied regardless of whether the device answered, refused, or stayed silent — for one simple rule instead of a per-outcome one. Real controllers leave several seconds here, and they appear to spend that time re-broadcasting 0x28 to look for more devices on the same channel, which this project's single-device discovery phase has no equivalent of. 300 ms keeps pairing fast; a Somfy Izymo dimmer pairs with both 300 and 5000. Raise it only for a device that answers the 0x2C but stays silent to the key exchange. Has no effect when pairing_discover_confirm is skip.
Which of the three channels the hub listens on while waiting for a discovery response.
A broadcast's answers are not pinned to the channel the request went out on: each device that answers keeps hopping on its own schedule, so the channel it happens to be on when it replies is largely independent of the one that carried the request. Somfy always-alive replies seem to never come back on the request channel, so the default skip_request spends the whole window on the two channels that carry nearly all of them — a third more dwell on each.
Set all when a device never answers discovery at all. That measurement comes from devices we already hear, so it cannot speak for one whose replies we might be missing entirely; all costs a third of the dwell on the other two channels and removes the only blind spot the default has.
Which way the system key moves after discovery. auto is the default.
auto picks the direction from the device's discovery answer. A device that answers as a remote controller (node type 1023) is pulled; any other device is pushed. The hub never switches to the other direction inside one attempt: a refusal repeats, and a retry costs pairing time. Force a direction with push or pull when auto guesses wrong.
push is the normal pairing: the hub sends its own key to the device, and the device confirms it.
pull asks the device for its own key instead. The hub sends a key request (0x38), the device answers with its key, and the hub then challenges the device to prove that it holds that key. A key request that draws no answer is sent again, without a gap and up to nine times in all for a mains-powered device, three times with a gap for a low_power one, within `exchange_total_budget_ms`. An Atlantic AGILIA IO radiator in its display's pairing mode answers discovery as a remote controller and refuses a pushed key with COMMAND_UNKNOWN, so auto pulls it (issue #165).
The device's answer to the challenge is a 6-byte code, and only the right key produces it, so a match proves the key. The hub challenges up to three times while no answer comes back; an answer that does not match ends the pull. When no answer comes back at all, devices that answer a roll-call under the key prove it instead (see below). The log shows one Key pull check line with the key's key check value: six hex digits that identify a key without revealing it, so the line is safe to post. Two attempts with the same check value mean the device kept its key; two devices with the same check value share one key.
What happens next depends on that check:
A device that answers the key request with an error does not hand out its key: the advisor reports key_pull_not_offered. Set pairing_key_transfer: push to pair it the usual way.
To look for the devices that hold a pulled key, the hub first reads the address one above an Atlantic remote-controller node directly: one authenticated read of the heater's maximum setpoint. An Atlantic radiator's heater answers that read reliably but a roll-call only now and then. When the read gets no answer, or the node is not an Atlantic remote controller, the hub runs a roll-call under the key instead (about 13 seconds). See ADR 0058.
Pulling is proven on three Atlantic AGILIA IO radiators, each of which hands over its own key and proves it (issue #165, Atlantic AGILIA IO). A Freebox Delta in its "Share your network" mode also hands over its key when pulled, but never answers the challenge; seven of the reporter's devices answered the roll-call under it, which proved the key (issue #215).
The "Last Pairing Result" sensor that comes with the Discover & Pair button summarises every attempt in one machine-readable line. Its lbt and advice fields are the two most useful while tuning: a high lbt count with a channel_busy advice code means the channel, not the parameters above, is the bottleneck. The full field reference and advice codes are in Diagnosing a failed pairing attempt.
lbt_max_retries and lbt_rssi_threshold_dbm exist for diagnostics only. Setting the threshold too high (e.g. -45 dBm) or the retry count too low can force transmissions on a busy channel and may violate local regulations for the 868 MHz SRD band. Do not leave aggressive LBT values in a production configuration.