|
Home IO Control
ESPHome add-on for IO-Homecontrol devices
|
This project is still experimental. 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. It is not possible to test against every device that exists in the field, so getting pairing to work reliably on a specific device sometimes needs a bit of 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 | 117.3 | 58.6 / 78.2 / 117.3 / 156.2 / 187.2 (kHz) | Receiver bandwidth; wider tolerates post-TX frequency offset. |
| 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; 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 while hopping during discovery. |
| sx1262_discovery_hop_slice_ms | SX1262 | 200 | 50–500 ms | Per-channel dwell while hopping during discovery. |
| exchange_start_response_wait_ms | both | 1000 | 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. |
| lr1121_rx_bandwidth | LR1121 | 117.3 | 39.0 / 46.9 / 58.6 / 78.2 / 117.3 / 156.2 / 187.2 (kHz) | Receiver bandwidth; wider tolerates post-TX frequency offset. |
| 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 | 200 | 50–500 ms | Per-channel dwell while hopping during discovery. |
| 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 | Address the discovery frames are sent to. |
| 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_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 three roll-call attempts the scan_paired_devices action makes. |
| 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. |
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 discovery or key-exchange replies fail to decode cleanly on an SX1262 board.
Observations: the SX1262's local oscillator needs frequency headroom to recover after a TX→RX turnaround. Experiments here found that at 58.6 kHz the demodulator corrupted roughly half the bytes of a post-transmit frame (pairing succeeded only about one attempt in five); widening to 117.3 kHz removed the problem entirely. Going wider still (156.2/187.2) can help when a device's transmitter drifts more than the controller's own radio, at the cost of more noise. 58.6 is close to the narrow default used on the older SX1276 chip (≈41.7 kHz), but is marginal on the SX1262's tight turnaround.
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.
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: reply latency is a property of the device model, not of the radio, and the spread is enormous. A third-party capture of one network (2026-08-14, three controllers) measured a Somfy RS100 replying at 29 ms, 781 ms, 1548 ms, 1945 ms, 2469 ms and 3052 ms within a few minutes, while a Somfy Oximo 40 on the same network answered in ~23 ms every time.
Both of those are solar actuators, so don't try to predict this from the power source — the Oximo stays fast at night, the RS100 does not. Nor is it stable per device: the RS100's own spread covers two orders of magnitude, and it drifts slower over the course of a day. Treat the setting as something to measure per installation, not to infer.
The start-frame default used to be 300, shorter than the continuation default of 500 even though its own comment promised "longer" — backwards for the one case where the target may have been asleep until the 213 ms wake-up preamble reached it. Against 300 ms the RS100 was reachable only in its fastest state, so it answered intermittently and got worse as the day went on. Both radios failed identically, which is the signature of a chip-neutral protocol constant rather than a radio problem. The default is now 1000.
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.
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 default 41.7 kHz is the long-standing fixed value — tighter than the ~77 kHz Carson-rule figure, chosen to maximise sensitivity by rejecting out-of-band noise, and validated against real devices. Unlike the SX1262, the SX1276 has a fast TX→RX turnaround and has worked reliably at this narrow 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 SX1276 originally reused the protocol's 8-byte short preamble here. A slightly longer 12 bytes was found on hardware to improve the peer's lock-on with no measurable timing cost, so that is now the default — actually 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 simply 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 during the discovery wait. Change these when a device is clearly present but its discovery response 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 (this was the single biggest discovery fix). Known implementations dwell only a few milliseconds per channel, extending the dwell when a preamble is detected — the SX1276 uses that fast cycle. The SX1262 needs a much longer per-channel dwell (~200 ms) because its shorter-preamble responses are harder to catch mid-hop.
The LR1121 equivalents of the four SX1262 knobs above — same meaning, same defaults, 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).
Observations: the defaults were seeded from SX1262's validated values and are now confirmed working on real LR1121 hardware — authenticated open/close/stop exchanges complete reliably against a real awning at the stock 117.3 kHz / 8 B / 500 µs / 200 ms settings. Two of lr1121_rx_bandwidth's enum values (39.0/46.9 kHz) turned out to have the wrong register encoding when borrowed directly from SX1262 and were corrected — the full, corrected option set is 39.0 / 46.9 / 58.6 / 78.2 / 117.3 / 156.2 / 187.2 kHz. 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; there was no need to touch these defaults to get a working exchange.
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.
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.
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 docs/home_io_control.md's "Home Assistant Actions" section.
Observations: for the devices tested here, plain 0x28 to 0x00003B is what works — the alternate 0x2E drew no response from them (but they already answer 0x28). Full-featured controllers are known to broadcast both commands throughout pairing, which is exactly why the combined preset exists: a device that ignores 0x28 may only be reachable via 0x2E. In the Home Assistant UI the selector offers the combination (0x28,0x2E) as a preset.
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.
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.
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 Home Assistant action (see docs/home_io_control.md's "Home Assistant Actions" section); since that action makes three attempts, one per channel, its total runtime is roughly 3x this value.
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.
Every home_io_control config with a button: entity gets a companion "Last Pairing Result" diagnostic text sensor that publishes a frozen, machine-readable summary after each pairing attempt (leading v1; is a version tag — any later format change bumps it):
lbt (LBT retries consumed) and advice (see below) are the two fields most useful while tuning: a high lbt count with a channel_busy advice code means the channel — not the tuning parameters above — is the bottleneck. See docs/home_io_control.md's "Diagnosing a failed pairing attempt" section for the full field reference and the pairing-window traffic advisor's advice codes (1w_traffic, channel_busy, foreign_controller, rf_silent).
If a device that should be in pairing mode does not pair with the defaults, work through the following in order, changing one thing at a time and checking the logs after each step. This is a general starting point, not a guarantee — different devices need different combinations.
After each step, record the tuning snapshot line and the outcome. When a combination works, paste that snapshot into your permanent tuning: block. If you find a combination that makes a previously-unsupported device work, it is worth sharing via an issue on the projects Github-Page so the defaults can improve.
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.