From 9957840dfc7544eec0369fd5cec9a4a190dc8ea5 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Wed, 29 Jan 2025 11:00:18 +0100 Subject: [PATCH] Add multicast support to udp component (#8051) --- esphome/components/network/ip_address.h | 2 ++ esphome/components/udp/__init__.py | 6 ++++++ esphome/components/udp/udp_component.cpp | 18 ++++++++++++++++++ esphome/components/udp/udp_component.h | 3 +++ esphome/config_validation.py | 9 +++++++++ tests/components/udp/common.yaml | 1 + 6 files changed, 39 insertions(+) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 69d3788ca5..1598daf6f9 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -49,6 +49,7 @@ struct IPAddress { } IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } + std::string str() const { return str_lower_case(inet_ntoa(ip_addr_)); } #else IPAddress() { ip_addr_set_zero(&ip_addr_); } IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { @@ -119,6 +120,7 @@ struct IPAddress { bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr) bool is_ip4() { return IP_IS_V4(&ip_addr_); } bool is_ip6() { return IP_IS_V6(&ip_addr_); } + bool is_multicast() { return ip_addr_ismulticast(&ip_addr_); } std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); } bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); } bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); } diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index e189975ade..5485663f1c 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -27,6 +27,7 @@ UDPComponent = udp_ns.class_("UDPComponent", cg.PollingComponent) CONF_BROADCAST = "broadcast" CONF_BROADCAST_ID = "broadcast_id" CONF_ADDRESSES = "addresses" +CONF_LISTEN_ADDRESS = "listen_address" CONF_PROVIDER = "provider" CONF_PROVIDERS = "providers" CONF_REMOTE_ID = "remote_id" @@ -84,6 +85,9 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(UDPComponent), cv.Optional(CONF_PORT, default=18511): cv.port, + cv.Optional( + CONF_LISTEN_ADDRESS, default="255.255.255.255" + ): cv.ipv4address_multi_broadcast, cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( cv.ipv4address, ), @@ -154,5 +158,7 @@ async def to_code(config): for provider in config.get(CONF_PROVIDERS, ()): name = provider[CONF_NAME] cg.add(var.add_provider(name)) + if (listen_address := str(config[CONF_LISTEN_ADDRESS])) != "255.255.255.255": + cg.add(var.set_listen_address(listen_address)) if encryption := provider.get(CONF_ENCRYPTION): cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index e29620fa9a..30f7356879 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -249,6 +249,21 @@ void UDPComponent::setup() { server.sin_addr.s_addr = ESPHOME_INADDR_ANY; server.sin_port = htons(this->port_); + if (this->listen_address_.has_value()) { + struct ip_mreq imreq = {}; + imreq.imr_interface.s_addr = ESPHOME_INADDR_ANY; + inet_aton(this->listen_address_.value().str().c_str(), &imreq.imr_multiaddr); + server.sin_addr.s_addr = imreq.imr_multiaddr.s_addr; + ESP_LOGV(TAG, "Join multicast %s", this->listen_address_.value().str().c_str()); + err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)); + if (err < 0) { + ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno); + this->mark_failed(); + this->status_set_error("Failed to set IP_ADD_MEMBERSHIP"); + return; + } + } + err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); @@ -565,6 +580,9 @@ void UDPComponent::dump_config() { ESP_LOGCONFIG(TAG, " Ping-pong: %s", YESNO(this->ping_pong_enable_)); for (const auto &address : this->addresses_) ESP_LOGCONFIG(TAG, " Address: %s", address.c_str()); + if (this->listen_address_.has_value()) { + ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str().c_str()); + } #ifdef USE_SENSOR for (auto sensor : this->sensors_) ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id); diff --git a/esphome/components/udp/udp_component.h b/esphome/components/udp/udp_component.h index b4e11cf652..fb9b93e255 100644 --- a/esphome/components/udp/udp_component.h +++ b/esphome/components/udp/udp_component.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/components/network/ip_address.h" #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif @@ -69,6 +70,7 @@ class UDPComponent : public PollingComponent { } #endif void add_address(const char *addr) { this->addresses_.emplace_back(addr); } + void set_listen_address(const char *listen_addr) { this->listen_address_ = network::IPAddress(listen_addr); } void set_port(uint16_t port) { this->port_ = port; } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -143,6 +145,7 @@ class UDPComponent : public PollingComponent { std::map> remote_binary_sensors_{}; #endif + optional listen_address_{}; std::map providers_{}; std::vector ping_header_{}; std::vector header_{}; diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 20a0774ccb..27d11e4ded 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1168,6 +1168,15 @@ def ipv4address(value): return address +def ipv4address_multi_broadcast(value): + address = ipv4address(value) + if not (address.is_multicast or (address == IPv4Address("255.255.255.255"))): + raise Invalid( + f"{value} is not a multicasst address nor local broadcast address" + ) + return address + + def ipaddress(value): try: address = ip_address(value) diff --git a/tests/components/udp/common.yaml b/tests/components/udp/common.yaml index 3bdc19ece5..e533cb965e 100644 --- a/tests/components/udp/common.yaml +++ b/tests/components/udp/common.yaml @@ -7,6 +7,7 @@ udp: encryption: "our key goes here" rolling_code_enable: true ping_pong_enable: true + listen_address: 239.0.60.53 binary_sensors: - binary_sensor_id1 - id: binary_sensor_id1