mirror of
https://github.com/esphome/esphome.git
synced 2026-02-08 08:41:59 +00:00
[udp] Store addresses in flash instead of heap
This commit is contained in:
@@ -108,8 +108,7 @@ async def to_code(config):
|
||||
cg.add(var.set_broadcast_port(conf_port[CONF_BROADCAST_PORT]))
|
||||
if (listen_address := str(config[CONF_LISTEN_ADDRESS])) != "255.255.255.255":
|
||||
cg.add(var.set_listen_address(listen_address))
|
||||
for address in config[CONF_ADDRESSES]:
|
||||
cg.add(var.add_address(str(address)))
|
||||
cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]]))
|
||||
if on_receive := config.get(CONF_ON_RECEIVE):
|
||||
on_receive = on_receive[0]
|
||||
trigger = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID])
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "udp_component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace udp {
|
||||
namespace esphome::udp {
|
||||
|
||||
static const char *const TAG = "udp";
|
||||
|
||||
@@ -95,7 +94,7 @@ void UDPComponent::setup() {
|
||||
// 8266 and RP2040 `Duino
|
||||
for (const auto &address : this->addresses_) {
|
||||
auto ipaddr = IPAddress();
|
||||
ipaddr.fromString(address.c_str());
|
||||
ipaddr.fromString(address);
|
||||
this->ipaddrs_.push_back(ipaddr);
|
||||
}
|
||||
if (this->should_listen_)
|
||||
@@ -130,8 +129,8 @@ void UDPComponent::dump_config() {
|
||||
" Listen Port: %u\n"
|
||||
" Broadcast Port: %u",
|
||||
this->listen_port_, this->broadcast_port_);
|
||||
for (const auto &address : this->addresses_)
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", address.c_str());
|
||||
for (const char *address : this->addresses_)
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", address);
|
||||
if (this->listen_address_.has_value()) {
|
||||
char addr_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str_to(addr_buf));
|
||||
@@ -162,7 +161,6 @@ void UDPComponent::send_packet(const uint8_t *data, size_t size) {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} // namespace udp
|
||||
} // namespace esphome
|
||||
} // namespace esphome::udp
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_NETWORK
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
#include "esphome/components/socket/socket.h"
|
||||
@@ -9,15 +10,17 @@
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
#include <WiFiUdp.h>
|
||||
#endif
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace udp {
|
||||
namespace esphome::udp {
|
||||
|
||||
static const size_t MAX_PACKET_SIZE = 508;
|
||||
class UDPComponent : public Component {
|
||||
public:
|
||||
void add_address(const char *addr) { this->addresses_.emplace_back(addr); }
|
||||
void set_addresses(std::initializer_list<const char *> addresses) { this->addresses_ = addresses; }
|
||||
/// Prevent accidental use of std::string which would dangle
|
||||
void set_addresses(std::initializer_list<std::string> addresses) = delete;
|
||||
void set_listen_address(const char *listen_addr) { this->listen_address_ = network::IPAddress(listen_addr); }
|
||||
void set_listen_port(uint16_t port) { this->listen_port_ = port; }
|
||||
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
|
||||
@@ -49,11 +52,10 @@ class UDPComponent : public Component {
|
||||
std::vector<IPAddress> ipaddrs_{};
|
||||
WiFiUDP udp_client_{};
|
||||
#endif
|
||||
std::vector<std::string> addresses_{};
|
||||
FixedVector<const char *> addresses_{};
|
||||
|
||||
optional<network::IPAddress> listen_address_{};
|
||||
};
|
||||
|
||||
} // namespace udp
|
||||
} // namespace esphome
|
||||
} // namespace esphome::udp
|
||||
#endif
|
||||
|
||||
@@ -5,7 +5,10 @@ wifi:
|
||||
udp:
|
||||
id: my_udp
|
||||
listen_address: 239.0.60.53
|
||||
addresses: ["239.0.60.53"]
|
||||
addresses:
|
||||
- "239.0.60.53"
|
||||
- "192.168.1.255"
|
||||
- "10.0.0.255"
|
||||
on_receive:
|
||||
- logger.log:
|
||||
format: "Received %d bytes"
|
||||
|
||||
33
tests/integration/fixtures/udp_send_receive.yaml
Normal file
33
tests/integration/fixtures/udp_send_receive.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
esphome:
|
||||
name: udp-test
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
services:
|
||||
- service: send_udp_message
|
||||
then:
|
||||
- udp.write:
|
||||
id: test_udp
|
||||
data: "HELLO_UDP_TEST"
|
||||
- service: send_udp_bytes
|
||||
then:
|
||||
- udp.write:
|
||||
id: test_udp
|
||||
data: [0x55, 0x44, 0x50, 0x5F, 0x42, 0x59, 0x54, 0x45, 0x53] # "UDP_BYTES"
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
udp:
|
||||
- id: test_udp
|
||||
addresses:
|
||||
- "127.0.0.1"
|
||||
- "127.0.0.2"
|
||||
port:
|
||||
listen_port: UDP_LISTEN_PORT_PLACEHOLDER
|
||||
broadcast_port: UDP_BROADCAST_PORT_PLACEHOLDER
|
||||
on_receive:
|
||||
- logger.log:
|
||||
format: "Received UDP: %d bytes"
|
||||
args: [data.size()]
|
||||
171
tests/integration/test_udp.py
Normal file
171
tests/integration/test_udp.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""Integration test for UDP component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
import contextlib
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass, field
|
||||
import socket
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@dataclass
|
||||
class UDPReceiver:
|
||||
"""Collects UDP messages received."""
|
||||
|
||||
messages: list[bytes] = field(default_factory=list)
|
||||
message_received: asyncio.Event = field(default_factory=asyncio.Event)
|
||||
|
||||
def on_message(self, data: bytes) -> None:
|
||||
"""Called when a message is received."""
|
||||
self.messages.append(data)
|
||||
self.message_received.set()
|
||||
|
||||
async def wait_for_message(self, timeout: float = 5.0) -> bytes:
|
||||
"""Wait for a message to be received."""
|
||||
await asyncio.wait_for(self.message_received.wait(), timeout=timeout)
|
||||
return self.messages[-1]
|
||||
|
||||
async def wait_for_content(self, content: bytes, timeout: float = 5.0) -> bytes:
|
||||
"""Wait for a specific message content."""
|
||||
deadline = asyncio.get_event_loop().time() + timeout
|
||||
while True:
|
||||
for msg in self.messages:
|
||||
if content in msg:
|
||||
return msg
|
||||
remaining = deadline - asyncio.get_event_loop().time()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError(
|
||||
f"Content {content!r} not found in messages: {self.messages}"
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(self.message_received.wait(), timeout=remaining)
|
||||
self.message_received.clear()
|
||||
except TimeoutError:
|
||||
raise TimeoutError(
|
||||
f"Content {content!r} not found in messages: {self.messages}"
|
||||
) from None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def udp_listener(port: int = 0) -> AsyncGenerator[tuple[int, UDPReceiver]]:
|
||||
"""Async context manager that listens for UDP messages.
|
||||
|
||||
Args:
|
||||
port: Port to listen on. 0 for auto-assign.
|
||||
|
||||
Yields:
|
||||
Tuple of (port, UDPReceiver) where port is the UDP port being listened on.
|
||||
"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(("127.0.0.1", port))
|
||||
sock.setblocking(False)
|
||||
actual_port = sock.getsockname()[1]
|
||||
|
||||
receiver = UDPReceiver()
|
||||
|
||||
async def receive_messages() -> None:
|
||||
"""Background task to receive UDP messages."""
|
||||
loop = asyncio.get_running_loop()
|
||||
while True:
|
||||
try:
|
||||
data = await loop.sock_recv(sock, 4096)
|
||||
if data:
|
||||
receiver.on_message(data)
|
||||
except BlockingIOError:
|
||||
await asyncio.sleep(0.01)
|
||||
except Exception:
|
||||
break
|
||||
|
||||
task = asyncio.create_task(receive_messages())
|
||||
try:
|
||||
yield actual_port, receiver
|
||||
finally:
|
||||
task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await task
|
||||
sock.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_udp_send_receive(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test UDP component can send messages with multiple addresses configured."""
|
||||
# Track log lines to verify dump_config output
|
||||
log_lines: list[str] = []
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
log_lines.append(line)
|
||||
|
||||
async with udp_listener() as (udp_port, receiver):
|
||||
# Replace placeholders in the config
|
||||
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(udp_port + 1))
|
||||
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(udp_port))
|
||||
|
||||
async with (
|
||||
run_compiled(config, line_callback=on_log_line),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify device is running
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "udp-test"
|
||||
|
||||
# Get services
|
||||
_, services = await client.list_entities_services()
|
||||
|
||||
# Test sending string message
|
||||
send_message_service = next(
|
||||
(s for s in services if s.name == "send_udp_message"), None
|
||||
)
|
||||
assert send_message_service is not None, (
|
||||
"send_udp_message service not found"
|
||||
)
|
||||
|
||||
await client.execute_service(send_message_service, {})
|
||||
|
||||
try:
|
||||
msg = await receiver.wait_for_content(b"HELLO_UDP_TEST", timeout=5.0)
|
||||
assert b"HELLO_UDP_TEST" in msg
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"UDP string message not received. Got: {receiver.messages}"
|
||||
)
|
||||
|
||||
# Test sending bytes
|
||||
send_bytes_service = next(
|
||||
(s for s in services if s.name == "send_udp_bytes"), None
|
||||
)
|
||||
assert send_bytes_service is not None, "send_udp_bytes service not found"
|
||||
|
||||
await client.execute_service(send_bytes_service, {})
|
||||
|
||||
try:
|
||||
msg = await receiver.wait_for_content(b"UDP_BYTES", timeout=5.0)
|
||||
assert b"UDP_BYTES" in msg
|
||||
except TimeoutError:
|
||||
pytest.fail(f"UDP bytes message not received. Got: {receiver.messages}")
|
||||
|
||||
# Verify we received at least 2 messages (string + bytes)
|
||||
assert len(receiver.messages) >= 2, (
|
||||
f"Expected at least 2 messages, got {len(receiver.messages)}"
|
||||
)
|
||||
|
||||
# Verify dump_config logged all configured addresses
|
||||
# This tests that FixedVector<const char*> stores addresses correctly
|
||||
log_text = "\n".join(log_lines)
|
||||
assert "Address: 127.0.0.1" in log_text, (
|
||||
f"Address 127.0.0.1 not found in dump_config. Log: {log_text[-2000:]}"
|
||||
)
|
||||
assert "Address: 127.0.0.2" in log_text, (
|
||||
f"Address 127.0.0.2 not found in dump_config. Log: {log_text[-2000:]}"
|
||||
)
|
||||
Reference in New Issue
Block a user