AboutBlogContact
Advanced & Guides

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

  1. Provider — the device that broadcasts its sensor/binary sensor states over UDP.
  2. 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.

Dimmer A — boneio-dimmer-a.yaml
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_01

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

Dimmer B — boneio-dimmer-b.yaml
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_01

Multi-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

Dimmer A — boneio-dimmer-a.yaml
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_01

Dimmer B — receives multi-click events, controls lights

Dimmer B — boneio-dimmer-b.yaml
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_03

How it works: Dimmer A detects the click pattern and briefly pulses (ON → 200ms → OFF) the corresponding template binary sensor. packet_transport broadcasts the state change. Dimmer B receives it and uses on_press to 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)

Input24 Gen2 — boneio-input24-gen2-01.yaml
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 providers section.

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.

32x10 Lights — add to boneio-32-l-07.yaml
# ──────────────────────────────────────────────
# 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_01

Scaling up: To react to more inputs from Input24, add more packet_transport binary sensors on 32x10 — e.g. in_02_state_input24light.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:

Dimmer A — provider
packet_transport:
  platform: udp
  update_interval: 30s
  sensors:
    - boneIO_temp          # id of the lm75b temperature sensor
Dimmer B — consumer
packet_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:

Both devices
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.yaml file and reference it with !secret udp_encryption_key.


Important Notes

Packet Transport supports only sensor and binary_sensor! The packet_transport component can only broadcast and receive sensor and binary_sensor entities. 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 udpaddresses list.
  • No delivery guarantee — UDP does not guarantee delivery. The packet_transport component re-sends data periodically (controlled by update_interval) to mitigate this.
  • Device names matter — the provider: name must exactly match the esphome: name of the other device. If the provider has name_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 in provider: 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.

See Also