UDP Communication Between Devices
How to connect two boneIO ESP devices using the ESPHome UDP component so they can share sensor states and synchronize inputs — without a server or broker.
UDP Communication Between Devices
The ESPHome UDP component, combined with Packet Transport, allows two (or more) boneIO ESP devices to communicate directly over the local network. This is useful when you want one device to react to an input or sensor state from another device — without going through Home Assistant or any other broker.
Common use cases:
- A wall switch on Dimmer A controls a light on Dimmer B
- A temperature sensor on one device is displayed or used on another
- Synchronizing binary sensor states between devices
How It Works
- Provider — the device that broadcasts its sensor/binary sensor states over UDP.
- Consumer — the device that receives those states and can act on them.
A single device can be both a provider and a consumer at the same time.
The data is sent via UDP broadcast on the local network (default port 18511). No IP configuration is needed for basic setups — all devices on the same subnet will receive the broadcast.
Prerequisites
- Two (or more) boneIO ESP devices on the same network.
- ESPHome 2025.6.0 or newer (UDP + Packet Transport support).
- Access to the ESPHome dashboard to edit YAML configs.
Example: Two boneIO Dimmer Gen2 Devices
In this scenario, we have two boneIO Dimmer Gen2 devices. We want Input 1 on Dimmer A to toggle Light CHL 01 on Dimmer B, and vice versa.
Network Diagram
┌──────────────────┐ UDP broadcast ┌──────────────────┐
│ Dimmer A │ ◄──────────────────────────► │ Dimmer B │
│ │ (port 18511) │ │
│ IN_01 ──► toggle│ │ IN_01 ──► toggle│
│ light on B │ │ light on A │
│ │ │ │
│ CHL 01 (light) │ │ CHL 01 (light) │
└──────────────────┘ └──────────────────┘Dimmer A Configuration
Add the following sections to your Dimmer A YAML config. The key parts are udp, packet_transport, and the binary_sensor with platform: packet_transport.
substitutions:
name: boneio-dimmer-a
friendly_name: "BoneIO Dimmer A"
# ... (standard esphome, esp32, ethernet, i2c, output, light sections)
# ──────────────────────────────────────────────
# UDP transport — broadcasts and receives on LAN
# ──────────────────────────────────────────────
udp:
# ──────────────────────────────────────────────
# Packet Transport — what to send and from whom to receive
# ──────────────────────────────────────────────
packet_transport:
platform: udp
update_interval: 5s
# Broadcast these binary sensors to other devices
binary_sensors:
- in_01_state_dimmer_a
# Declare providers we want to receive data from
providers:
- name: boneio-dimmer-b # must match the ESPHome 'name' of Dimmer B
# ──────────────────────────────────────────────
# Template binary sensor — mirrors IN_01 state for broadcasting
# ──────────────────────────────────────────────
binary_sensor:
# Physical input
- platform: gpio
name: "IN_01"
id: in_01
pin:
pcf8574: pcf_inputs
number: 0
mode:
input: true
inverted: true
on_press:
then:
- light.toggle: chl_01
# Template sensor that mirrors IN_01 — this gets broadcast via UDP
- platform: template
id: in_01_state_dimmer_a
lambda: "return id(in_01).state;"
# Receive IN_01 state from Dimmer B — toggle local light
- platform: packet_transport
provider: boneio-dimmer-b
id: in_01_state_dimmer_b # must match the broadcast id on Dimmer B
on_press:
then:
- light.toggle: chl_01Dimmer B Configuration
The configuration is symmetrical — Dimmer B broadcasts its own in_01_state_dimmer_b and listens for in_01_state_dimmer_a from Dimmer A.
substitutions:
name: boneio-dimmer-b
friendly_name: "BoneIO Dimmer B"
# ... (standard esphome, esp32, ethernet, i2c, output, light sections)
# ──────────────────────────────────────────────
# UDP transport
# ──────────────────────────────────────────────
udp:
# ──────────────────────────────────────────────
# Packet Transport
# ──────────────────────────────────────────────
packet_transport:
platform: udp
update_interval: 5s
binary_sensors:
- in_01_state_dimmer_b
providers:
- name: boneio-dimmer-a # must match the ESPHome 'name' of Dimmer A
# ──────────────────────────────────────────────
# Binary sensors
# ──────────────────────────────────────────────
binary_sensor:
# Physical input
- platform: gpio
name: "IN_01"
id: in_01
pin:
pcf8574: pcf_inputs
number: 0
mode:
input: true
inverted: true
on_press:
then:
- light.toggle: chl_01
# Template sensor — broadcast via UDP
- platform: template
id: in_01_state_dimmer_b
lambda: "return id(in_01).state;"
# Receive from Dimmer A
- platform: packet_transport
provider: boneio-dimmer-a
id: in_01_state_dimmer_a # must match the broadcast id on Dimmer A
on_press:
then:
- light.toggle: chl_01Multi-Click over UDP
When using on_multi_click with UDP, always process the timing on the source device (Dimmer A). The source device detects the click pattern locally and pulses a template binary sensor for each event type. The remote device (Dimmer B) receives these pulses via packet_transport and controls its lights accordingly.
Why not send the raw button state? Multi-click detection relies on precise timing. Sending raw press/release events over UDP would introduce network jitter and unreliable detection on the remote side. By resolving the click pattern locally, Dimmer B gets a clean, instant signal.
Dimmer A — detects multi-click, broadcasts results
udp:
packet_transport:
platform: udp
update_interval: 5s
binary_sensors:
- single_click_dimmer_a_in_01
- double_click_dimmer_a_in_01
- long_press_dimmer_a_in_01
providers:
- name: boneio-dimmer-b
light:
- platform: monochromatic
output: chl01
name: "CHL 01"
id: chl_01
default_transition_length: 2s
gamma_correct: 0
- platform: monochromatic
output: chl02
name: "CHL 02"
id: chl_02
default_transition_length: 2s
gamma_correct: 0
- platform: monochromatic
output: chl03
name: "CHL 03"
id: chl_03
default_transition_length: 2s
gamma_correct: 0
binary_sensor:
# Physical input with multi-click detection
- platform: gpio
name: "IN_01"
id: in_01
pin:
pcf8574: pcf_inputs
number: 0
mode:
input: true
inverted: true
on_multi_click:
# Long press
- timing:
- ON for at least 1.4s
then:
- logger.log: "Long press detected"
- light.toggle: chl_01
- binary_sensor.template.publish:
id: long_press_dimmer_a_in_01
state: ON
- delay: 200ms
- binary_sensor.template.publish:
id: long_press_dimmer_a_in_01
state: OFF
# Double click
- timing:
- ON for at most 1s
- OFF for at most 0.5s
- ON for at most 1s
- OFF for at least 0.2s
then:
- logger.log: "Double click detected"
- light.toggle: chl_02
- binary_sensor.template.publish:
id: double_click_dimmer_a_in_01
state: ON
- delay: 200ms
- binary_sensor.template.publish:
id: double_click_dimmer_a_in_01
state: OFF
# Single click
- timing:
- ON for at most 1s
- OFF for at least 0.5s
then:
- logger.log: "Single click detected"
- light.toggle: chl_03
- binary_sensor.template.publish:
id: single_click_dimmer_a_in_01
state: ON
- delay: 200ms
- binary_sensor.template.publish:
id: single_click_dimmer_a_in_01
state: OFF
# Template binary sensors — pulse ON/OFF to broadcast click events via UDP
- platform: template
id: single_click_dimmer_a_in_01
- platform: template
id: double_click_dimmer_a_in_01
- platform: template
id: long_press_dimmer_a_in_01Dimmer B — receives multi-click events, controls lights
udp:
packet_transport:
platform: udp
providers:
- name: boneio-dimmer-a
light:
- platform: monochromatic
output: chl01
name: "CHL 01"
id: chl_01
default_transition_length: 2s
gamma_correct: 0
- platform: monochromatic
output: chl02
name: "CHL 02"
id: chl_02
default_transition_length: 2s
gamma_correct: 0
- platform: monochromatic
output: chl03
name: "CHL 03"
id: chl_03
default_transition_length: 2s
gamma_correct: 0
binary_sensor:
# Receive click events from Dimmer A
- platform: packet_transport
provider: boneio-dimmer-a
id: long_press_dimmer_a_in_01
on_press:
then:
- light.toggle: chl_01
- platform: packet_transport
provider: boneio-dimmer-a
id: double_click_dimmer_a_in_01
on_press:
then:
- light.toggle: chl_02
- platform: packet_transport
provider: boneio-dimmer-a
id: single_click_dimmer_a_in_01
on_press:
then:
- light.toggle: chl_03How it works: Dimmer A detects the click pattern and briefly pulses (ON → 200ms → OFF) the corresponding template binary sensor.
packet_transportbroadcasts the state change. Dimmer B receives it and useson_pressto toggle the appropriate light.
Example: Input24 Gen2 → 32x10 Lights
A common setup — the boneIO Input24 Gen2 has 24 inputs but no light outputs, while the boneIO 32x10 Lights has 32 relay outputs but may not have enough local inputs. Using UDP you can press a button on Input24 and toggle a light on 32x10.
Network Diagram
┌──────────────────┐ UDP broadcast ┌──────────────────┐
│ Input24 Gen2 │ ─────────────────────────► │ 32x10 Lights │
│ (ESP32-S3) │ (port 18511) │ (ESP32) │
│ │ │ │
│ IN_01 ──► sends │ │ receives ──► │
│ state via UDP │ │ light.toggle │
│ │ │ Light 01 │
└──────────────────┘ └──────────────────┘Input24 Gen2 — full configuration (broadcasts all 24 inputs)
substitutions:
name: boneio-input24-gen2-01
friendly_name: "boneIO ESP Input24 Gen2"
serial_prefix: "esp24"
firmware_manifest: "https://boneio.eu/fwesp/boneio-input24-gen2-01.json"
esphome:
name: "${name}"
friendly_name: "${friendly_name}"
name_add_mac_suffix: true
project:
name: boneio.input24-gen2
version: "0.1"
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
ethernet:
id: eth
type: W5500
clk_pin: GPIO13
mosi_pin: GPIO39
miso_pin: GPIO38
cs_pin: GPIO12
interrupt_pin: GPIO2
reset_pin: GPIO1
clock_speed: 25MHz
i2c:
sda: GPIO10
scl: GPIO11
scan: true
frequency: 400kHz
uart:
id: boneio_uart
rx_pin: GPIO21
tx_pin: GPIO14
baud_rate: 9600
stop_bits: 1
modbus:
send_wait_time: 80ms
uart_id: boneio_uart
id: boneio_modbus
packages:
internals_packages:
url: https://github.com/boneIO-eu/esphome
ref: packages-v2.0.0
files: ["packages/devices/serial_no.yaml"]
dashboard_import:
package_import_url: github://boneIO-eu/esphome/boneio-input24_gen2-v0_1.yaml@latest
import_full_config: true
pcf8574:
- id: pcf_inputs_1_to_8
address: 0x38
- id: pcf_inputs_9_to_24
address: 0x20
pcf8575: true
logger:
hardware_uart: UART0
api:
reboot_timeout: 0s
ota:
- platform: esphome
- platform: web_server
web_server:
port: 80
version: 3
local: true
sensor:
- platform: lm75b
id: boneIO_temp
name: "Temperature"
update_interval: 30s
entity_category: diagnostic
on_value_range:
- above: 70.0
then:
- switch.turn_on: buzzer
- below: 70.0
then:
- switch.turn_off: buzzer
switch:
- platform: gpio
id: buzzer
name: "Buzzer"
pin:
number: GPIO9
mode:
output: true
inverted: false
# ──────────────────────────────────────────────
# UDP + Packet Transport — broadcast all 24 inputs
# ──────────────────────────────────────────────
udp:
packet_transport:
platform: udp
update_interval: 5s
binary_sensors:
- in_01_state_input24
- in_02_state_input24
- in_03_state_input24
- in_04_state_input24
- in_05_state_input24
- in_06_state_input24
- in_07_state_input24
- in_08_state_input24
- in_09_state_input24
- in_10_state_input24
- in_11_state_input24
- in_12_state_input24
- in_13_state_input24
- in_14_state_input24
- in_15_state_input24
- in_16_state_input24
- in_17_state_input24
- in_18_state_input24
- in_19_state_input24
- in_20_state_input24
- in_21_state_input24
- in_22_state_input24
- in_23_state_input24
- in_24_state_input24
# ──────────────────────────────────────────────
# Physical inputs (IN_01 to IN_08 — pcf_inputs_1_to_8)
# ──────────────────────────────────────────────
binary_sensor:
- platform: gpio
name: "IN_01"
id: in_01
pin:
pcf8574: pcf_inputs_1_to_8
number: 0
mode:
input: true
inverted: true
- platform: gpio
name: "IN_02"
id: in_02
pin:
pcf8574: pcf_inputs_1_to_8
number: 1
mode:
input: true
inverted: true
- platform: gpio
name: "IN_03"
id: in_03
pin:
pcf8574: pcf_inputs_1_to_8
number: 2
mode:
input: true
inverted: true
- platform: gpio
name: "IN_04"
id: in_04
pin:
pcf8574: pcf_inputs_1_to_8
number: 3
mode:
input: true
inverted: true
- platform: gpio
name: "IN_05"
id: in_05
pin:
pcf8574: pcf_inputs_1_to_8
number: 4
mode:
input: true
inverted: true
- platform: gpio
name: "IN_06"
id: in_06
pin:
pcf8574: pcf_inputs_1_to_8
number: 5
mode:
input: true
inverted: true
- platform: gpio
name: "IN_07"
id: in_07
pin:
pcf8574: pcf_inputs_1_to_8
number: 6
mode:
input: true
inverted: true
- platform: gpio
name: "IN_08"
id: in_08
pin:
pcf8574: pcf_inputs_1_to_8
number: 7
mode:
input: true
inverted: true
# ──────────────────────────────────────────────
# Physical inputs (IN_09 to IN_24 — pcf_inputs_9_to_24)
# ──────────────────────────────────────────────
- platform: gpio
name: "IN_09"
id: in_09
pin:
pcf8574: pcf_inputs_9_to_24
number: 7
mode:
input: true
inverted: true
- platform: gpio
name: "IN_10"
id: in_10
pin:
pcf8574: pcf_inputs_9_to_24
number: 6
mode:
input: true
inverted: true
- platform: gpio
name: "IN_11"
id: in_11
pin:
pcf8574: pcf_inputs_9_to_24
number: 5
mode:
input: true
inverted: true
- platform: gpio
name: "IN_12"
id: in_12
pin:
pcf8574: pcf_inputs_9_to_24
number: 4
mode:
input: true
inverted: true
- platform: gpio
name: "IN_13"
id: in_13
pin:
pcf8574: pcf_inputs_9_to_24
number: 3
mode:
input: true
inverted: true
- platform: gpio
name: "IN_14"
id: in_14
pin:
pcf8574: pcf_inputs_9_to_24
number: 2
mode:
input: true
inverted: true
- platform: gpio
name: "IN_15"
id: in_15
pin:
pcf8574: pcf_inputs_9_to_24
number: 1
mode:
input: true
inverted: true
- platform: gpio
name: "IN_16"
id: in_16
pin:
pcf8574: pcf_inputs_9_to_24
number: 0
mode:
input: true
inverted: true
- platform: gpio
name: "IN_17"
id: in_17
pin:
pcf8574: pcf_inputs_9_to_24
number: 15
mode:
input: true
inverted: true
- platform: gpio
name: "IN_18"
id: in_18
pin:
pcf8574: pcf_inputs_9_to_24
number: 14
mode:
input: true
inverted: true
- platform: gpio
name: "IN_19"
id: in_19
pin:
pcf8574: pcf_inputs_9_to_24
number: 13
mode:
input: true
inverted: true
- platform: gpio
name: "IN_20"
id: in_20
pin:
pcf8574: pcf_inputs_9_to_24
number: 12
mode:
input: true
inverted: true
- platform: gpio
name: "IN_21"
id: in_21
pin:
pcf8574: pcf_inputs_9_to_24
number: 11
mode:
input: true
inverted: true
- platform: gpio
name: "IN_22"
id: in_22
pin:
pcf8574: pcf_inputs_9_to_24
number: 10
mode:
input: true
inverted: true
- platform: gpio
name: "IN_23"
id: in_23
pin:
pcf8574: pcf_inputs_9_to_24
number: 9
mode:
input: true
inverted: true
- platform: gpio
name: "IN_24"
id: in_24
pin:
pcf8574: pcf_inputs_9_to_24
number: 8
mode:
input: true
inverted: true
# ──────────────────────────────────────────────
# Template sensors — mirror each input for UDP broadcast
# ──────────────────────────────────────────────
- platform: template
id: in_01_state_input24
lambda: "return id(in_01).state;"
- platform: template
id: in_02_state_input24
lambda: "return id(in_02).state;"
- platform: template
id: in_03_state_input24
lambda: "return id(in_03).state;"
- platform: template
id: in_04_state_input24
lambda: "return id(in_04).state;"
- platform: template
id: in_05_state_input24
lambda: "return id(in_05).state;"
- platform: template
id: in_06_state_input24
lambda: "return id(in_06).state;"
- platform: template
id: in_07_state_input24
lambda: "return id(in_07).state;"
- platform: template
id: in_08_state_input24
lambda: "return id(in_08).state;"
- platform: template
id: in_09_state_input24
lambda: "return id(in_09).state;"
- platform: template
id: in_10_state_input24
lambda: "return id(in_10).state;"
- platform: template
id: in_11_state_input24
lambda: "return id(in_11).state;"
- platform: template
id: in_12_state_input24
lambda: "return id(in_12).state;"
- platform: template
id: in_13_state_input24
lambda: "return id(in_13).state;"
- platform: template
id: in_14_state_input24
lambda: "return id(in_14).state;"
- platform: template
id: in_15_state_input24
lambda: "return id(in_15).state;"
- platform: template
id: in_16_state_input24
lambda: "return id(in_16).state;"
- platform: template
id: in_17_state_input24
lambda: "return id(in_17).state;"
- platform: template
id: in_18_state_input24
lambda: "return id(in_18).state;"
- platform: template
id: in_19_state_input24
lambda: "return id(in_19).state;"
- platform: template
id: in_20_state_input24
lambda: "return id(in_20).state;"
- platform: template
id: in_21_state_input24
lambda: "return id(in_21).state;"
- platform: template
id: in_22_state_input24
lambda: "return id(in_22).state;"
- platform: template
id: in_23_state_input24
lambda: "return id(in_23).state;"
- platform: template
id: in_24_state_input24
lambda: "return id(in_24).state;"Note: Input24 Gen2 has no light outputs, so it only acts as a provider. It does not need a
providerssection.
32x10 Lights — consumer (receives IN_01 from Input24, toggles Light 01)
On the 32x10 side, only the UDP/Packet Transport sections need to be added to the existing configuration. The rest of the config (lights, local inputs) stays unchanged.
# ──────────────────────────────────────────────
# UDP + Packet Transport
# ──────────────────────────────────────────────
udp:
packet_transport:
platform: udp
providers:
- name: boneio-input24-gen2-01 # must match the ESPHome 'name' of Input24
# Add this to your existing binary_sensor section:
binary_sensor:
# ... (keep all existing local inputs IN_01..IN_32 unchanged)
# Receive IN_01 from Input24 Gen2 — toggle Light 01
- platform: packet_transport
provider: boneio-input24-gen2-01
id: in_01_state_input24
on_press:
then:
- light.toggle: light_01Scaling up: To react to more inputs from Input24, add more
packet_transportbinary sensors on 32x10 — e.g.in_02_state_input24→light.toggle: light_02, etc. All 24 inputs are already being broadcast.
Sharing Sensor Data (e.g. Temperature)
You can also share sensor values between devices. For example, broadcasting the onboard temperature sensor from Dimmer A so Dimmer B can display it:
packet_transport:
platform: udp
update_interval: 30s
sensors:
- boneIO_temp # id of the lm75b temperature sensorpacket_transport:
platform: udp
providers:
- name: boneio-dimmer-a
sensor:
- platform: packet_transport
provider: boneio-dimmer-a
id: boneIO_temp # same id as on Dimmer A
name: "Dimmer A Temperature"Adding Encryption
For added security, you can encrypt the UDP communication. Both sides must use the same encryption key:
packet_transport:
platform: udp
encryption: "my-secret-key-here"
rolling_code_enable: true
# ... rest of config
providers:
- name: boneio-dimmer-b
encryption: "my-secret-key-here"Tip: Store the encryption key in your
secrets.yamlfile and reference it with!secret udp_encryption_key.
Important Notes
Packet Transport supports only
sensorandbinary_sensor! Thepacket_transportcomponent can only broadcast and receivesensorandbinary_sensorentities. It does not support lights, switches, covers, events, or any other entity types. This is why we use template binary sensors as "signal carriers" to transmit click events between devices (see the Multi-Click example above).
- Same network required — UDP broadcast works only within the same subnet. If your devices are on different VLANs, you need to specify explicit IP addresses in the
udp→addresseslist. - No delivery guarantee — UDP does not guarantee delivery. The
packet_transportcomponent re-sends data periodically (controlled byupdate_interval) to mitigate this. - Device names matter — the
provider: namemust exactly match theesphome: nameof the other device. If the provider hasname_add_mac_suffix: true, the actual device name will have a MAC address suffix appended (e.g.boneio-input24-gen2-01-a1b2c3). You can find the full name in the ESPHome logs at boot or in the Home Assistant device name. Use that full name inprovider: name. - Unique IDs — each device should broadcast under a unique id (e.g.
in_01_state_dimmer_a,in_01_state_dimmer_b) to avoid confusion. The consumer references the remote id by matching the provider name + id. - ESPHome version — make sure both devices run ESPHome 2025.6.0+ for full UDP + Packet Transport support.