Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
Radio tuning

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.

Note
Use these carefully. They change real RF and protocol behaviour. Aggressive or mismatched values can cause failed or partial pairing, leave a device in an unexpected state, or produce transmissions the device rejects. Change one thing at a time, watch the logs, and revert anything that does not help. When you are done, keep only the values that made a difference in your configuration.

Enabling the tuning UI

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.

Note
Tuned values are not persisted. Every value resets to its default on reboot. Once you find a combination that works, copy it from the log snapshot into your YAML tuning: block so it survives a restart. You can also set any parameter directly in YAML (without ui_controls) once you know what you want.

How to experiment

  1. Turn on verbose logging. Set the logger to DEBUG so you can see the pairing exchange and the raw packet capture:

    logger:
    level: DEBUG
    # Or keep the global level higher and raise only these tags:
    # logs:
    # home_io_control: DEBUG
    # home_io_control.exchange: DEBUG
    # io_capture: DEBUG # per-frame TX/RX packet capture
  2. Change one parameter at a time. With several changes at once you cannot tell which one mattered. Adjust a single value, trigger Discover & Pair, and read the result.
  3. Watch the logs for impact. Useful signals:
    • io_capture … stage=tx_frame … — a frame the hub transmitted.
    • a discovery response (0x29) — the device heard a discovery command.
    • saw_challenge=1 in the exchange summary — the device answered the key-exchange start.
    • the Exchange failed … stage=… line — where in the flow it stopped.
    • the src= and dst= of incoming frames — a device in pairing mode announces itself and reveals the address it is active on (often 0x00003F); that address is a strong hint for where to aim discovery. Be careful to confirm the src= is the device you are pairing: remotes and other devices share the air and may look like the target. Disconnecting other controllers during the test removes that noise.
  4. Identify the target's own address first. Before tuning, operate the device (or put it in pairing mode) and watch which src= address it transmits from and which dst= it targets. If the device you want never transmits at all, no discovery command will reach it — the issue is the device's pairing mode, not a tuning value.
  5. Expect a brief block during each attempt. A pairing attempt runs to completion in one go, so a took a long time for an operation (… ms) warning is normal and harmless. Its duration grows with pairing_discovery_wait_ms × the number of commands × the fixed per-command retries, so keep pairing_discovery_wait_ms modest when trying several commands at once.
  6. Save your logs. Keep a copy of the logs for each experiment (for example esphome logs … > pairing-attempt-01.log). Together with the one-line tuning snapshot the component prints, this lets you compare attempts later and reconstruct exactly which change produced which result.

At boot and at the start of every pairing attempt, the component logs the active overrides:

[home_io_control] Tuning overrides active: pairing_discovery_commands=[0x28,0x2E] sx1262_post_tx_settle_us=750

or, when nothing is overridden:

[home_io_control] Tuning: defaults active

Each UI change is also logged in YAML-compatible form, ready to paste back:

[home_io_control] Tuning updated via HA: sx1262_post_tx_settle_us=750

Parameter reference

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.

Note
Background — channels & hopping. The protocol uses three 868 MHz channels (≈868.25 / 868.95 / 869.85 MHz). A unicast reply comes back on the channel the request went out on, so those waits hold still — pairing's key-challenge/key-confirm waits and every command's wait for its first and final response alike. A broadcast roll-call reply is different, and which channels its wait covers depends on the pass: the scan_paired_devices always-alive pass skips the request channel (replies there are rare for that population), while its low-power pass listens on all three, including the request channel, because low-power replies have been observed landing there. Either way the wait lingers on any channel where it detects an incoming preamble. Several parameters below tune that wait — but if a device is never heard at all, the cause is usually the command/address, not the timing.

Quick reference

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 8 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_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 8 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.
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_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 carrier-sense attempts before TX.
lbt_rssi_threshold_dbm both -90 -95 to -70 dBm RSSI below which the channel counts as free.
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 Address the discovery frames are sent to. The last two are lighting-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.

ui_controls itself is a feature toggle (default false) that exposes these as entities; it is not a tunable.

Radio / physical layer — in detail

sx1262_rx_bandwidth

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.

sx1262_response_preamble

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. Already-paired devices lock onto an on-air preamble of 8 bytes reliably. Brand-new devices often fail to decode on-air preambles of 1/4/8 bytes and need something substantially longer — exactly how much, 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 8 as untested rather than known-good. The same "not below the tens of bytes" caution is why normal_start_preamble (below) defaults to 32 rather than 8.

sx1262_post_tx_settle_us

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.

exchange_start_response_wait_ms / exchange_response_wait_ms

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. Raise it for a stubborn device; lower it if slow failures are worse for you than missed commands.

exchange_total_budget_ms

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 there is still budget left for it.

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.

sx1276_rx_bandwidth

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.

sx1276_response_preamble

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. That is also longer than the SX1262's 8 (see the SX1262 section above: brand-new devices needed noticeably more than that on the SX1262, so don't read the SX1276's smaller number as evidence 12 is generous; it has never needed raising in testing here). Lengthen it further for a stubborn or marginal-range device.

sx1276_discovery_hop_slice_ms / sx1262_discovery_hop_slice_ms

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.

lr1121_rx_bandwidth / lr1121_response_preamble / lr1121_post_tx_settle_us / lr1121_discovery_hop_slice_ms

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.

cold_broadcast_reply_preamble

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.

normal_start_preamble

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.

low_power_wake_belief

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 that is asleep needs the 1024-byte wake-up preamble. One that is awake — 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 was recently moving or heard from, and orders the tries of each directed exchange accordingly:

Believed Try 1 Try 2 Try 3
awake (a stop, or a move the device accepted in the last 2 minutes that is not yet known to have ended) short wake-up short
maybe awake (heard from in the last 30 s) short wake-up wake-up
asleep wake-up wake-up wake-up

"Short" is `normal_start_preamble`. A wrong belief costs one try, not the exchange, because every plan still sends the wake-up preamble at least once. A status poll allowed only one try (most scheduled polls) sends the first try's preamble; if it misses, the next poll a few seconds later has three tries. The poll right after an accepted stop always gets all three. Always-alive devices are unaffected, and the frame itself never changes between tries.

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):

Try 1 ended: no first response for cmd=PRIVATE(0x03) within 400 ms preamble=32 age_ms=6712
Try 3 answered: cmd=PRIVATE(0x03) wait_ms=231 preamble=1024 age_ms=8190
Auth challenge try=1 wait_ms=26 req_cmd=0x00 req_len=6 preamble=32 age_ms=1034

Off restores the old behaviour: the wake-up preamble on every try to a low_power: device.

lbt_max_retries / lbt_rssi_threshold_dbm

Listen-Before-Talk: before transmitting, the hub checks the channel is quieter than the threshold, retrying up to max_retries times, then transmits anyway. 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.

Warning
Regulatory note: the -90 dBm / ≥5 ms defaults follow the 868 MHz band's listen-before-talk rules. Loosening them is acceptable for a brief experiment but should not be left in a production configuration.

Pairing protocol — in detail

pairing_discovery_commands

Which discovery command(s) the hub broadcasts, and in what order; it sends them in sequence and stops at the first device response.

  • 0x28 — standard 2W broadcast discovery, sent to 0x00003B.
  • 0x2E — alternate discovery, sent to 0x00003F — the address on which devices in a 1W-triggered pairing mode (and their beacons) listen. Conventionally paired with payload 0x00 and low_power on.

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.

pairing_discovery_destination

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.

pairing_discovery_payload / pairing_discovery_low_power

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.

pairing_discovery_ack_capable

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.

pairing_discovery_preamble

The preamble length (in bytes) on the discovery broadcast itself (0x28/0x2E). Defaults to 1024 (LONG_PREAMBLE): a factory-fresh device in learning mode is exactly the kind of duty-cycled receiver that wake-up burst exists for.

Some receivers never lock onto a preamble this long while they are awake, which is why every directed start frame derives its preamble from the target's power class (see normal_start_preamble above). The discovery broadcast can't do that: discovery runs before anything is known about the device, so it uses this setting.

A VELUX SSL solar roller shutter needs 32. It answers nothing at 1024 and answers within ~80 ms at 32. Somfy devices answer either way. Try it for any genuinely unpaired device that doesn't answer 0x28 (see A tuning plan):

tuning:
  ui_controls: true
  pairing_discovery_preamble: 32   # NORMAL_START_PREAMBLE's value; try 8 (SHORT_PREAMBLE) too

It also shortens 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 this setting. The device has just answered a discovery with this 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). The log says so when it happens (Pairing: directed frames to … use the 32-byte discovery preamble it just answered). At the default 1024 nothing changes. 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`).

pairing_discovery_wait_ms / pairing_discovery_initial_dwell_ms

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.

pairing_key_exchange_retries

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.

scan_power_classes

Which device power classes the scan_paired_devices roll-call calls (see Scan Paired Devices):

  • both (default): a low-power pass (long wake-up preamble, all three channels) followed by the always-alive pass (short preamble, the two channels not used for that transmit) — six attempts total, roughly 13 seconds.
  • always_alive: only the always-alive pass — three attempts, the ~6 s single-pass scan. Use this on an installation with no solar or battery devices; the report only gains one extra NOTE: line naming the excluded class.
  • low_power: only the low-power pass — three attempts, roughly 6 seconds. Use this to test in isolation whether a sleeping device answers the low-power call at all, without the always-alive pass's replies muddying the result.

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.

pairing_discover_confirm

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).

  • send (default): sends 0x2C with CTRL1_ACK ("sender can handle 2W responses") clear in both cases — 0x00 to an always-alive target, 0x20 (CTRL1_LOW_POWER only) to a low-power one, matching every corpus hub's own shape for a low-power target. Setting CTRL1_ACK unconditionally has silenced real Somfy awnings before (see pairing_discovery_ack_capable above), so it stays off for an always-alive target here too, even though no captured hub sends that shape. Hardware-confirmed: a Somfy Izymo dimmer answers send's 0x2C within ~25 ms. A short pause (pairing_key_init_delay_ms) follows the step before 0x31 is sent, whether or not the device answered.
  • send_with_ack: like send, but sets CTRL1_ACK for an always-alive target too (0x10) — the shape captured VELUX hubs (KLR200, KIG300) and a Somfy Connectivity Kit use. A low-power target still gets 0x20, identical to send. The Somfy Izymo dimmer never answers this shape (pairing still completes, after the full ~4.8 s no-answer wait), so don't use it for Somfy devices. It is only worth trying for an always-alive device (typically mains-powered VELUX) that answers discovery but never sends a 0x2D with send, and whose key exchange then fails.
  • skip: kill switch. Sends no 0x2C and applies no pause. Use this if the step ever appears to cause a pairing regression.

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.

pairing_key_init_delay_ms

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.

pairing_discovery_listen_channels

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.

Reading pairing results without the tuning UI

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.

Safety and compliance

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.

See also

  • Diagnostic probes — asking a device directly what it answers to
  • A tuning plan — these parameters in the order to try them
  • Pairing — what the discovery parameters above are tuning