A custom ESPHome component that creates a UDP server to receive messages and send responses. This component was tested and works on an esp32 with arduino framework.
- 🔌 Listen for UDP packets on a configurable port
- 📨 Receive string data via UDP
- 🔄 Send UDP responses back to the sender
- 🎯 Trigger ESPHome automations on data reception
- 🔍 Access sender's IP address and port in lambda functions
- 🔒 IP address filtering/whitelist
- 🔎 Text content filtering (contains, starts_with, ends_with, equals)
- ⚡ Non-blocking continuous operation with loop() monitoring
- 🎭 Multiple triggers with different filters per UDP port
Add this to your ESPHome configuration:
external_components:
- source: github://jsimonetti/esphome-udpserver
components: [ udpserver ]- Download or clone this repository
- Copy the
components/udpserverfolder to your ESPHome configuration directory undercustom_components/
cd /path/to/your/esphome/config
git submodule add https://github.com/jsimonetti/esphome-udpserver.git custom_components/udpserver-repo
ln -s custom_components/udpserver-repo/components/udpserver custom_components/udpserverudpserver:
listen_port: 8888 # Port to listen on (0-65535)
allowed_ips: # Optional: IP whitelist (if omitted, all IPs allowed)
- "192.168.1.100"
- "192.168.1.101"
on_string_data: # Trigger when data is received
- text_filter: "CMD:" # Optional: only trigger if text contains/matches
filter_mode: CONTAINS # Optional: CONTAINS, STARTS_WITH, ENDS_WITH, EQUALS
then:
- lambda: |-
// Access the received data
ESP_LOGD("udp", "Received: %s", data.c_str());
// Access sender information
ESP_LOGD("udp", "From: %s:%d", udp.get_remote_ip(), udp.get_remote_port());
// Send a response back to the sender
udp.send_response("Message received!");listen_port(Required, int): Port number to listen on (0-65535)allowed_ips(Optional, list): Whitelist of IP addresses allowed to send messages. If omitted, all IPs are allowed.on_string_data(Optional, automation): Triggered when UDP data is receivedtext_filter(Optional, string): Text pattern to matchfilter_mode(Optional, enum): How to match the text filterCONTAINS: Message must contain the filter text (default)STARTS_WITH: Message must start with the filter textENDS_WITH: Message must end with the filter textEQUALS: Message must exactly match the filter text
When the on_string_data trigger fires, the following variables are available:
data(std::string): The received UDP message as a stringudp(UDPContext): Context object for accessing sender info and sending responses
-
udp.send_response(std::string data): Send a UDP response back to the sender- Returns:
bool- true if sent successfully, false otherwise
- Returns:
-
udp.get_remote_ip(): Get the sender's IP address- Returns:
const char*- IP address as string
- Returns:
-
udp.get_remote_port(): Get the sender's port number- Returns:
uint16_t- Port number
- Returns:
udpserver:
listen_port: 8888
on_string_data:
- lambda: |-
udp.send_response("ECHO: " + data);udpserver:
listen_port: 8888
allowed_ips:
- "192.168.1.100" # Admin workstation
- "192.168.1.101" # Monitoring server
on_string_data:
- lambda: |-
udp.send_response("Authorized: " + data);udpserver:
listen_port: 9000
on_string_data:
# Handle commands that start with "CMD:"
- text_filter: "CMD:"
filter_mode: STARTS_WITH
then:
- lambda: |-
std::string cmd = data.substr(4); // Remove "CMD:" prefix
ESP_LOGI("udp", "Executing: %s", cmd.c_str());
udp.send_response("OK");
# Handle queries that end with "?"
- text_filter: "?"
filter_mode: ENDS_WITH
then:
- lambda: |-
udp.send_response("Answer: 42");
# Handle exact "ping" command
- text_filter: "ping"
filter_mode: EQUALS
then:
- lambda: |-
udp.send_response("pong");udpserver:
listen_port: 9999
on_string_data:
- lambda: |-
if (data == "ping") {
udp.send_response("pong");
} else if (data == "status") {
udp.send_response("OK");
} else {
udp.send_response("ERROR: Unknown command");switch:
- platform: gpio
pin: GPIO2
id: relay1
udpserver:
listen_port: 8888
on_string_data:
- lambda: |-
if (data == "on") {
id(relay1).turn_on();
udp.send_response("Relay ON");
} else if (data == "off") {
id(relay1).turn_off();
udp.send_response("Relay OFF");
}udpserver:
listen_port: 9999
allowed_ips:
- "192.168.1.10" # Admin workstation only
on_string_data:
- text_filter: "ADMIN:"
filter_mode: STARTS_WITH
then:
- lambda: |-
std::string cmd = data.substr(6);
ESP_LOGI("admin", "Admin command from %s: %s",
udp.get_remote_ip(), cmd.c_str());
if (cmd == "restart") {
udp.send_response("Restarting...");
ESP.restart();
} else {
udp.send_response("Unknown admin command");
}udpserver:
listen_port: 8888
on_string_data:
- lambda: |-
std::string response = "{\"status\":\"ok\",\"message\":\"" + data + "\"}";
udp.send_response(response);# Basic test (wait 1 second for response)
echo "ping" | nc -u -w1 192.168.1.100 8888
# Send with specific source port
echo "hello" | nc -p 54321 -u 192.168.1.100 8888
# Interactive mode (type commands and see responses)
nc -u 192.168.1.100 8888
# Send multiple commands
echo "CMD:turn_on" | nc -u -w1 192.168.1.100 8888
echo "status?" | nc -u -w1 192.168.1.100 8888
echo "relay_status" | nc -u -w1 192.168.1.100 9999
# Send without waiting for response
echo "relay_on" | nc -u -N 192.168.1.100 9999import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(2.0)
# Send message
sock.sendto(b"ping", ("192.168.1.100", 8888))
# Receive response
response, addr = sock.recvfrom(1024)
print(f"Response: {response.decode()}")
sock.close()const dgram = require('dgram');
const client = dgram.createSocket('udp4');
client.on('message', (msg, rinfo) => {
console.log(`Response: ${msg}`);
client.close();
});
client.send('ping', 8888, '192.168.1.100', (err) => {
if (err) console.error(err);
});- Component Type: Component with continuous loop() monitoring
- Base Classes: Inherits from ESPHome's
Component - Dependencies: Requires
networkcomponent (WiFi or Ethernet) - Initialization: UDP socket starts after network connection is established
When a UDP packet is received:
- The component parses the packet and extracts data
- A
UDPContextobject is created with the sender's IP/port and UDP socket reference - All registered triggers are fired with the data and context
- Lambda functions can call
udp.send_response()to send data back - The response is sent to the original sender's IP and port
- Maximum packet size: 1400 bytes (configurable in source)
- UDP is connectionless - no delivery guarantee
- Responses are sent to the sender of the most recent packet
- Multiple simultaneous connections are handled sequentially
Contributions are welcome! Please ensure code follows ESPHome conventions and includes proper error handling.