From d6c2dd3c26fb26fce9d7dfa495b594e2ba4687c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jan 2026 08:21:16 -1000 Subject: [PATCH 1/5] [wifi] Eliminate heap allocations in IP address logging (#13017) --- esphome/components/wifi/wifi_component.cpp | 4 +-- .../wifi/wifi_component_esp8266.cpp | 28 ++++++------------- .../wifi/wifi_component_libretiny.cpp | 13 ++------- 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e1dc2d17d6..2d635d893f 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -639,13 +639,13 @@ void WiFiComponent::setup_ap_config_() { } this->ap_setup_ = this->wifi_start_ap_(this->ap_); - auto ip_address = this->wifi_soft_ap_ip().str(); + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, "Setting up AP:\n" " AP SSID: '%s'\n" " AP Password: '%s'\n" " IP Address: %s", - this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str()); + this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), this->wifi_soft_ap_ip().str_to(ip_buf)); #ifdef USE_WIFI_MANUAL_IP auto manual_ip = this->ap_.get_manual_ip(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 9d99e0b94c..b7d820413c 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -371,7 +371,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { while (!connected) { uint8_t ipv6_addr_count = 0; for (auto addr : addrList) { - ESP_LOGV(TAG, "Address %s", addr.toString().c_str()); + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGV(TAG, "Address %s", network::IPAddress(addr.ipFromNetifNum()).str_to(ip_buf)); if (addr.isV6()) { ipv6_addr_count++; } @@ -413,21 +414,6 @@ const LogString *get_auth_mode_str(uint8_t mode) { return LOG_STR("UNKNOWN"); } } -#ifdef ipv4_addr -std::string format_ip_addr(struct ipv4_addr ip) { - char buf[20]; - sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16), - uint8_t(ip.addr >> 24)); - return buf; -} -#else -std::string format_ip_addr(struct ip_addr ip) { - char buf[20]; - sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16), - uint8_t(ip.addr >> 24)); - return buf; -} -#endif const LogString *get_op_mode_str(uint8_t mode) { switch (mode) { case WIFI_OFF: @@ -582,8 +568,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } case EVENT_STAMODE_GOT_IP: { auto it = event->event_info.got_ip; - ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(), - format_ip_addr(it.mask).c_str()); + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE], gw_buf[network::IP_ADDRESS_BUFFER_SIZE], + mask_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", network::IPAddress(&it.ip).str_to(ip_buf), + network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf)); s_sta_got_ip = true; #ifdef USE_WIFI_LISTENERS for (auto *listener : global_wifi_component->ip_state_listeners_) { @@ -635,8 +623,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE auto it = event->event_info.distribute_sta_ip; char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; format_mac_addr_upper(it.mac, mac_buf); - ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", mac_buf, format_ip_addr(it.ip).c_str(), it.aid); + ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", mac_buf, network::IPAddress(&it.ip).str_to(ip_buf), + it.aid); #endif break; } diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index e9ccb86871..68fcc3577d 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -231,14 +231,6 @@ const char *get_auth_mode_str(uint8_t mode) { } } -using esphome_ip4_addr_t = IPAddress; - -std::string format_ip4_addr(const esphome_ip4_addr_t &ip) { - char buf[20]; - uint32_t addr = ip; - sprintf(buf, "%u.%u.%u.%u", uint8_t(addr >> 0), uint8_t(addr >> 8), uint8_t(addr >> 16), uint8_t(addr >> 24)); - return buf; -} const char *get_op_mode_str(uint8_t mode) { switch (mode) { case WIFI_OFF: @@ -530,8 +522,9 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: { - ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), - format_ip4_addr(WiFi.gatewayIP()).c_str()); + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE], gw_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGV(TAG, "static_ip=%s gateway=%s", network::IPAddress(WiFi.localIP()).str_to(ip_buf), + network::IPAddress(WiFi.gatewayIP()).str_to(gw_buf)); s_sta_state = LTWiFiSTAState::CONNECTED; #ifdef USE_WIFI_LISTENERS for (auto *listener : this->ip_state_listeners_) { From 8eb28a7724798fae4a1b6d0ee5203ee8221fdffb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jan 2026 08:38:39 -1000 Subject: [PATCH 2/5] [neopixelbus] Fix ESP8266 compilation by enabling Serial/Serial1 for NeoPixelBus library (#13027) --- esphome/components/neopixelbus/light.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index d071059185..c77217243c 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -194,6 +194,14 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + if CORE.is_esp8266: + # NeoPixelBus library unconditionally includes NeoEsp8266UartMethod.h + # which references Serial and Serial1, so we must enable both + from esphome.components.esp8266.const import enable_serial, enable_serial1 + + enable_serial() + enable_serial1() + has_white = "W" in config[CONF_TYPE] method = config[CONF_METHOD] From 4419bf02b1864976d67fdcc0407f70f80cdbd7fe Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Tue, 6 Jan 2026 21:26:27 +0100 Subject: [PATCH 3/5] [async_tcp] Fix build conflicts and use IDF component for ESP32 (#13025) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .clang-tidy.hash | 2 +- esphome/components/async_tcp/__init__.py | 6 +++--- esphome/components/async_tcp/async_tcp.h | 5 ++--- esphome/components/async_tcp/async_tcp_socket.cpp | 5 +++-- esphome/components/async_tcp/async_tcp_socket.h | 6 +++--- esphome/idf_component.yml | 2 ++ platformio.ini | 1 - 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 59caddf59b..0a71b6859f 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -97fb425f1d681a5994ed1cc6187910f5d2c37ee577b6dc07eb3f4d8862a011de +191a0e6ab5842d153dd77a2023bc5742f9d4333c334de8d81b57f2b8d4d4b65e diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 4b6c6a275c..1ff4805f03 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -26,12 +26,12 @@ CONFIG_SCHEMA = cv.Schema({}) @coroutine_with_priority(CoroPriority.NETWORK_TRANSPORT) async def to_code(config): - if CORE.using_esp_idf: - # ESP-IDF needs the IDF component + if CORE.is_esp32: + # https://github.com/ESP32Async/AsyncTCP from esphome.components.esp32 import add_idf_component add_idf_component(name="esp32async/asynctcp", ref="3.4.91") - elif CORE.is_esp32 or CORE.is_libretiny: + elif CORE.is_libretiny: # https://github.com/ESP32Async/AsyncTCP cg.add_library("ESP32Async/AsyncTCP", "3.4.5") elif CORE.is_esp8266: diff --git a/esphome/components/async_tcp/async_tcp.h b/esphome/components/async_tcp/async_tcp.h index 362f603451..6d9211f023 100644 --- a/esphome/components/async_tcp/async_tcp.h +++ b/esphome/components/async_tcp/async_tcp.h @@ -1,9 +1,8 @@ #pragma once #include "esphome/core/defines.h" -#if (defined(USE_ESP32) || defined(USE_LIBRETINY)) && !defined(CLANG_TIDY) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) // Use AsyncTCP library for ESP32 (Arduino or ESP-IDF) and LibreTiny -// But not for clang-tidy as the header file isn't present in that case #include #elif defined(USE_ESP8266) // Use ESPAsyncTCP library for ESP8266 (always Arduino) @@ -12,6 +11,6 @@ // Use AsyncTCP_RP2040W library for RP2040 #include #else -// Use socket-based implementation for other platforms and clang-tidy +// Use socket-based implementation for other platforms #include "async_tcp_socket.h" #endif diff --git a/esphome/components/async_tcp/async_tcp_socket.cpp b/esphome/components/async_tcp/async_tcp_socket.cpp index 6c13f346e9..f64e494f5f 100644 --- a/esphome/components/async_tcp/async_tcp_socket.cpp +++ b/esphome/components/async_tcp/async_tcp_socket.cpp @@ -1,6 +1,7 @@ #include "async_tcp_socket.h" -#if defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) +#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \ + (defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)) #include "esphome/components/network/util.h" #include "esphome/core/log.h" @@ -158,4 +159,4 @@ void AsyncClient::loop() { } // namespace esphome::async_tcp -#endif // defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) +#endif diff --git a/esphome/components/async_tcp/async_tcp_socket.h b/esphome/components/async_tcp/async_tcp_socket.h index ca3bf19d67..28714a7752 100644 --- a/esphome/components/async_tcp/async_tcp_socket.h +++ b/esphome/components/async_tcp/async_tcp_socket.h @@ -2,7 +2,8 @@ #include "esphome/core/defines.h" -#if defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) +#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \ + (defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)) #include "esphome/components/socket/socket.h" #include @@ -69,5 +70,4 @@ class AsyncClient { // Expose AsyncClient in global namespace to match library behavior using esphome::async_tcp::AsyncClient; // NOLINT(google-global-names-in-headers) -#define ESPHOME_ASYNC_TCP_SOCKET_IMPL -#endif // defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) +#endif diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 36aa77c524..2dc5b94847 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -31,3 +31,5 @@ dependencies: version: 0.2.2 rules: - if: "target in [esp32, esp32s2, esp32s3, esp32p4]" + esp32async/asynctcp: + version: 3.4.91 diff --git a/platformio.ini b/platformio.ini index dd9eb566c5..d96e9ad2cc 100644 --- a/platformio.ini +++ b/platformio.ini @@ -146,7 +146,6 @@ lib_deps = WiFi ; wifi,web_server_base,ethernet (Arduino built-in) Update ; ota,web_server_base (Arduino built-in) ${common:arduino.lib_deps} - ESP32Async/AsyncTCP@3.4.5 ; async_tcp NetworkClientSecure ; http_request,nextion (Arduino built-in) HTTPClient ; http_request,nextion (Arduino built-in) ESPmDNS ; mdns (Arduino built-in) From 412ab5dbbf681f47f99cad7a69f9b8854dff34cb Mon Sep 17 00:00:00 2001 From: Jas Strong Date: Tue, 6 Jan 2026 13:31:50 -0800 Subject: [PATCH 4/5] [aqi] Implement a sensor that computes AQI (#12958) Co-authored-by: jas --- esphome/components/aqi/aqi_calculator.h | 27 ++++++----- esphome/components/aqi/aqi_sensor.cpp | 52 ++++++++++++++++++++++ esphome/components/aqi/aqi_sensor.h | 31 +++++++++++++ esphome/components/aqi/caqi_calculator.h | 25 +++++------ esphome/components/aqi/sensor.py | 51 +++++++++++++++++++++ esphome/components/hm3301/sensor.py | 9 ++++ esphome/components/pmsx003/pmsx003.cpp | 7 --- esphome/components/pmsx003/pmsx003.h | 11 ----- esphome/components/pmsx003/sensor.py | 26 ----------- tests/components/aqi/common.yaml | 22 +++++++++ tests/components/aqi/test.esp32-idf.yaml | 1 + tests/components/aqi/test.esp8266-ard.yaml | 1 + tests/components/aqi/test.rp2040-ard.yaml | 1 + tests/components/pmsx003/common.yaml | 3 -- 14 files changed, 193 insertions(+), 74 deletions(-) create mode 100644 esphome/components/aqi/aqi_sensor.cpp create mode 100644 esphome/components/aqi/aqi_sensor.h create mode 100644 esphome/components/aqi/sensor.py create mode 100644 tests/components/aqi/common.yaml create mode 100644 tests/components/aqi/test.esp32-idf.yaml create mode 100644 tests/components/aqi/test.esp8266-ard.yaml create mode 100644 tests/components/aqi/test.rp2040-ard.yaml diff --git a/esphome/components/aqi/aqi_calculator.h b/esphome/components/aqi/aqi_calculator.h index 959d6a2438..35dc35a44a 100644 --- a/esphome/components/aqi/aqi_calculator.h +++ b/esphome/components/aqi/aqi_calculator.h @@ -10,38 +10,37 @@ namespace esphome::aqi { class AQICalculator : public AbstractAQICalculator { public: uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { - int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); - int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); + int pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); + int pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; } protected: - static const int AMOUNT_OF_LEVELS = 6; + static constexpr int NUM_LEVELS = 6; - int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; + static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; - int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 9}, {10, 35}, {36, 55}, - {56, 125}, {126, 225}, {226, INT_MAX}}; + static constexpr int PM2_5_GRID[NUM_LEVELS][2] = {{0, 9}, {10, 35}, {36, 55}, {56, 125}, {126, 225}, {226, INT_MAX}}; - int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, - {255, 354}, {355, 424}, {425, INT_MAX}}; + static constexpr int PM10_0_GRID[NUM_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, + {255, 354}, {355, 424}, {425, INT_MAX}}; - int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { - int grid_index = get_grid_index_(value, array); + static int calculate_index(uint16_t value, const int array[NUM_LEVELS][2]) { + int grid_index = get_grid_index(value, array); if (grid_index == -1) { return -1; } - int aqi_lo = index_grid_[grid_index][0]; - int aqi_hi = index_grid_[grid_index][1]; + int aqi_lo = INDEX_GRID[grid_index][0]; + int aqi_hi = INDEX_GRID[grid_index][1]; int conc_lo = array[grid_index][0]; int conc_hi = array[grid_index][1]; return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo; } - int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { - for (int i = 0; i < AMOUNT_OF_LEVELS; i++) { + static int get_grid_index(uint16_t value, const int array[NUM_LEVELS][2]) { + for (int i = 0; i < NUM_LEVELS; i++) { if (value >= array[i][0] && value <= array[i][1]) { return i; } diff --git a/esphome/components/aqi/aqi_sensor.cpp b/esphome/components/aqi/aqi_sensor.cpp new file mode 100644 index 0000000000..cdc9f35ba6 --- /dev/null +++ b/esphome/components/aqi/aqi_sensor.cpp @@ -0,0 +1,52 @@ +#include "aqi_sensor.h" +#include "esphome/core/log.h" + +namespace esphome::aqi { + +static const char *const TAG = "aqi"; + +void AQISensor::setup() { + if (this->pm_2_5_sensor_ != nullptr) { + this->pm_2_5_sensor_->add_on_state_callback([this](float value) { + this->pm_2_5_value_ = value; + // Defer calculation to avoid double-publishing if both sensors update in the same loop + this->defer("update", [this]() { this->calculate_aqi_(); }); + }); + } + if (this->pm_10_0_sensor_ != nullptr) { + this->pm_10_0_sensor_->add_on_state_callback([this](float value) { + this->pm_10_0_value_ = value; + this->defer("update", [this]() { this->calculate_aqi_(); }); + }); + } +} + +void AQISensor::dump_config() { + ESP_LOGCONFIG(TAG, "AQI Sensor:"); + ESP_LOGCONFIG(TAG, " Calculation Type: %s", this->aqi_calc_type_ == AQI_TYPE ? "AQI" : "CAQI"); + if (this->pm_2_5_sensor_ != nullptr) { + ESP_LOGCONFIG(TAG, " PM2.5 Sensor: '%s'", this->pm_2_5_sensor_->get_name().c_str()); + } + if (this->pm_10_0_sensor_ != nullptr) { + ESP_LOGCONFIG(TAG, " PM10 Sensor: '%s'", this->pm_10_0_sensor_->get_name().c_str()); + } + LOG_SENSOR(" ", "AQI", this); +} + +void AQISensor::calculate_aqi_() { + if (std::isnan(this->pm_2_5_value_) || std::isnan(this->pm_10_0_value_)) { + return; + } + + AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); + if (calculator == nullptr) { + ESP_LOGW(TAG, "Unknown AQI calculator type"); + return; + } + + uint16_t aqi = + calculator->get_aqi(static_cast(this->pm_2_5_value_), static_cast(this->pm_10_0_value_)); + this->publish_state(aqi); +} + +} // namespace esphome::aqi diff --git a/esphome/components/aqi/aqi_sensor.h b/esphome/components/aqi/aqi_sensor.h new file mode 100644 index 0000000000..a990f815fe --- /dev/null +++ b/esphome/components/aqi/aqi_sensor.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "aqi_calculator_factory.h" + +namespace esphome::aqi { + +class AQISensor : public sensor::Sensor, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_pm_2_5_sensor(sensor::Sensor *sensor) { this->pm_2_5_sensor_ = sensor; } + void set_pm_10_0_sensor(sensor::Sensor *sensor) { this->pm_10_0_sensor_ = sensor; } + void set_aqi_calculation_type(AQICalculatorType type) { this->aqi_calc_type_ = type; } + + protected: + void calculate_aqi_(); + + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + AQICalculatorType aqi_calc_type_{AQI_TYPE}; + AQICalculatorFactory aqi_calculator_factory_; + + float pm_2_5_value_{NAN}; + float pm_10_0_value_{NAN}; +}; + +} // namespace esphome::aqi diff --git a/esphome/components/aqi/caqi_calculator.h b/esphome/components/aqi/caqi_calculator.h index d493dcdf39..9906c179f6 100644 --- a/esphome/components/aqi/caqi_calculator.h +++ b/esphome/components/aqi/caqi_calculator.h @@ -1,6 +1,5 @@ #pragma once -#include "esphome/core/log.h" #include "abstract_aqi_calculator.h" namespace esphome::aqi { @@ -8,37 +7,37 @@ namespace esphome::aqi { class CAQICalculator : public AbstractAQICalculator { public: uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { - int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); - int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); + int pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); + int pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; } protected: - static const int AMOUNT_OF_LEVELS = 5; + static constexpr int NUM_LEVELS = 5; - int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; + static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; - int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 15}, {16, 30}, {31, 55}, {56, 110}, {111, 400}}; + static constexpr int PM2_5_GRID[NUM_LEVELS][2] = {{0, 15}, {16, 30}, {31, 55}, {56, 110}, {111, 400}}; - int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 90}, {91, 180}, {181, 400}}; + static constexpr int PM10_0_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 90}, {91, 180}, {181, 400}}; - int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { - int grid_index = get_grid_index_(value, array); + static int calculate_index(uint16_t value, const int array[NUM_LEVELS][2]) { + int grid_index = get_grid_index(value, array); if (grid_index == -1) { return -1; } - int aqi_lo = index_grid_[grid_index][0]; - int aqi_hi = index_grid_[grid_index][1]; + int aqi_lo = INDEX_GRID[grid_index][0]; + int aqi_hi = INDEX_GRID[grid_index][1]; int conc_lo = array[grid_index][0]; int conc_hi = array[grid_index][1]; return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo; } - int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { - for (int i = 0; i < AMOUNT_OF_LEVELS; i++) { + static int get_grid_index(uint16_t value, const int array[NUM_LEVELS][2]) { + for (int i = 0; i < NUM_LEVELS; i++) { if (value >= array[i][0] && value <= array[i][1]) { return i; } diff --git a/esphome/components/aqi/sensor.py b/esphome/components/aqi/sensor.py new file mode 100644 index 0000000000..0b5ee8d75a --- /dev/null +++ b/esphome/components/aqi/sensor.py @@ -0,0 +1,51 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_PM_2_5, + CONF_PM_10_0, + DEVICE_CLASS_AQI, + STATE_CLASS_MEASUREMENT, +) + +from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns + +CODEOWNERS = ["@jasstrong"] +DEPENDENCIES = ["sensor"] + +UNIT_INDEX = "index" + +AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + AQISensor, + unit_of_measurement=UNIT_INDEX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.Required(CONF_PM_2_5): cv.use_id(sensor.Sensor), + cv.Required(CONF_PM_10_0): cv.use_id(sensor.Sensor), + cv.Required(CONF_CALCULATION_TYPE): cv.enum( + AQI_CALCULATION_TYPE, upper=True + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + + pm_2_5_sensor = await cg.get_variable(config[CONF_PM_2_5]) + cg.add(var.set_pm_2_5_sensor(pm_2_5_sensor)) + + pm_10_0_sensor = await cg.get_variable(config[CONF_PM_10_0]) + cg.add(var.set_pm_10_0_sensor(pm_10_0_sensor)) + + cg.add(var.set_aqi_calculation_type(config[CONF_CALCULATION_TYPE])) diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 389da97b1e..9546ae1c3c 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -1,3 +1,5 @@ +import logging + import esphome.codegen as cg from esphome.components import i2c, sensor from esphome.components.aqi import AQI_CALCULATION_TYPE, CONF_AQI, CONF_CALCULATION_TYPE @@ -16,6 +18,8 @@ from esphome.const import ( UNIT_MICROGRAMS_PER_CUBIC_METER, ) +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ["i2c"] AUTO_LOAD = ["aqi"] CODEOWNERS = ["@freekode"] @@ -99,7 +103,12 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_PM_10_0]) cg.add(var.set_pm_10_0_sensor(sens)) + # Remove before 2026.12.0 if CONF_AQI in config: + _LOGGER.warning( + "The 'aqi' option in hm3301 is deprecated, " + "please use the standalone 'aqi' sensor platform instead." + ) sens = await sensor.new_sensor(config[CONF_AQI]) cg.add(var.set_aqi_sensor(sens)) cg.add(var.set_aqi_calculation_type(config[CONF_AQI][CONF_CALCULATION_TYPE])) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 3bdb5219ed..bb167033d1 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -265,13 +265,6 @@ void PMSX003Component::parse_data_() { if (this->pm_particles_25um_sensor_ != nullptr) this->pm_particles_25um_sensor_->publish_state(pm_particles_25um); - // Calculate and publish AQI if sensor is configured - if (this->aqi_sensor_ != nullptr) { - aqi::AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); - int32_t aqi_value = calculator->get_aqi(pm_2_5_concentration, pm_10_0_concentration); - this->aqi_sensor_->publish_state(aqi_value); - } - if (this->type_ == PMSX003_TYPE_5003T) { ESP_LOGD(TAG, "Got PM0.3 Particles: %u Count/0.1L, PM0.5 Particles: %u Count/0.1L, PM1.0 Particles: %u Count/0.1L, " diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index 229972e2e5..f48121800e 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -4,7 +4,6 @@ #include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -#include "esphome/components/aqi/aqi_calculator_factory.h" namespace esphome { namespace pmsx003 { @@ -74,10 +73,6 @@ class PMSX003Component : public uart::UARTDevice, public Component { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } - void set_aqi_sensor(sensor::Sensor *aqi_sensor) { aqi_sensor_ = aqi_sensor; } - - void set_aqi_calculation_type(aqi::AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; } - protected: optional check_byte_(); void parse_data_(); @@ -121,12 +116,6 @@ class PMSX003Component : public uart::UARTDevice, public Component { // Temperature and Humidity sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; - - // AQI - sensor::Sensor *aqi_sensor_{nullptr}; - - aqi::AQICalculatorType aqi_calc_type_; - aqi::AQICalculatorFactory aqi_calculator_factory_ = aqi::AQICalculatorFactory(); }; } // namespace pmsx003 diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index b2d6744547..bebd3a01ee 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -1,6 +1,5 @@ import esphome.codegen as cg from esphome.components import sensor, uart -from esphome.components.aqi import AQI_CALCULATION_TYPE, CONF_AQI, CONF_CALCULATION_TYPE import esphome.config_validation as cv from esphome.const import ( CONF_FORMALDEHYDE, @@ -21,7 +20,6 @@ from esphome.const import ( CONF_TEMPERATURE, CONF_TYPE, CONF_UPDATE_INTERVAL, - DEVICE_CLASS_AQI, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, @@ -37,13 +35,11 @@ from esphome.const import ( CODEOWNERS = ["@ximex"] DEPENDENCIES = ["uart"] -AUTO_LOAD = ["aqi"] pmsx003_ns = cg.esphome_ns.namespace("pmsx003") PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Component) PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor) -UNIT_INDEX = "index" TYPE_PMSX003 = "PMSX003" TYPE_PMS5003T = "PMS5003T" TYPE_PMS5003ST = "PMS5003ST" @@ -81,10 +77,6 @@ def validate_pmsx003_sensors(value): for key, types in SENSORS_TO_TYPE.items(): if key in value and value[CONF_TYPE] not in types: raise cv.Invalid(f"{value[CONF_TYPE]} does not have {key} sensor!") - if CONF_AQI in value and CONF_PM_2_5 not in value: - raise cv.Invalid("AQI computation requires PM 2.5 sensor") - if CONF_AQI in value and CONF_PM_10_0 not in value: - raise cv.Invalid("AQI computation requires PM 10 sensor") return value @@ -200,19 +192,6 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_AQI): sensor.sensor_schema( - unit_of_measurement=UNIT_INDEX, - icon=ICON_CHEMICAL_WEAPON, - accuracy_decimals=0, - device_class=DEVICE_CLASS_AQI, - state_class=STATE_CLASS_MEASUREMENT, - ).extend( - { - cv.Required(CONF_CALCULATION_TYPE): cv.enum( - AQI_CALCULATION_TYPE, upper=True - ), - } - ), cv.Optional(CONF_UPDATE_INTERVAL, default="0s"): validate_update_interval, } ) @@ -299,9 +278,4 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity_sensor(sens)) - if CONF_AQI in config: - sens = await sensor.new_sensor(config[CONF_AQI]) - cg.add(var.set_aqi_sensor(sens)) - cg.add(var.set_aqi_calculation_type(config[CONF_AQI][CONF_CALCULATION_TYPE])) - cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) diff --git a/tests/components/aqi/common.yaml b/tests/components/aqi/common.yaml new file mode 100644 index 0000000000..4c8cbbfa3f --- /dev/null +++ b/tests/components/aqi/common.yaml @@ -0,0 +1,22 @@ +sensor: + - platform: template + id: pm25_sensor + name: "PM2.5" + lambda: "return 25.0;" + + - platform: template + id: pm10_sensor + name: "PM10" + lambda: "return 50.0;" + + - platform: aqi + name: "Air Quality Index (AQI)" + pm_2_5: pm25_sensor + pm_10_0: pm10_sensor + calculation_type: AQI + + - platform: aqi + name: "Air Quality Index (CAQI)" + pm_2_5: pm25_sensor + pm_10_0: pm10_sensor + calculation_type: CAQI diff --git a/tests/components/aqi/test.esp32-idf.yaml b/tests/components/aqi/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/aqi/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/aqi/test.esp8266-ard.yaml b/tests/components/aqi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/aqi/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/aqi/test.rp2040-ard.yaml b/tests/components/aqi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/aqi/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pmsx003/common.yaml b/tests/components/pmsx003/common.yaml index 9dd79723d1..3c60995804 100644 --- a/tests/components/pmsx003/common.yaml +++ b/tests/components/pmsx003/common.yaml @@ -25,7 +25,4 @@ sensor: name: Particulate Count >5.0um pm_10_0um: name: Particulate Count >10.0um - aqi: - name: AQI - calculation_type: AQI update_interval: 30s From 2147ddf8c7f25fac1a941b1980afdd69ac9349bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jan 2026 11:32:23 -1000 Subject: [PATCH 5/5] [api] Eliminate std::string from ClientInfo struct (#12566) Co-authored-by: Keith Burzinski --- esphome/components/api/api_connection.cpp | 42 +++++----- esphome/components/api/api_connection.h | 19 ++--- esphome/components/api/api_frame_helper.cpp | 10 ++- esphome/components/api/api_frame_helper.h | 28 ++++--- .../components/api/api_frame_helper_noise.cpp | 7 +- .../components/api/api_frame_helper_noise.h | 4 +- .../api/api_frame_helper_plaintext.cpp | 8 +- .../api/api_frame_helper_plaintext.h | 3 +- esphome/components/api/api_server.cpp | 14 ++-- .../components/esphome/ota/ota_esphome.cpp | 5 +- .../components/socket/bsd_sockets_impl.cpp | 40 +-------- .../components/socket/lwip_raw_tcp_impl.cpp | 57 ++++--------- .../components/socket/lwip_sockets_impl.cpp | 38 +-------- esphome/components/socket/socket.cpp | 81 +++++++++++++++++++ esphome/components/socket/socket.h | 20 ++++- .../voice_assistant/voice_assistant.cpp | 5 +- 16 files changed, 200 insertions(+), 181 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 27344a53ec..30f7b5710c 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -101,16 +101,14 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) auto &noise_ctx = parent->get_noise_ctx(); if (noise_ctx.has_psk()) { - this->helper_ = - std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)}; + this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx)}; } else { - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; } #elif defined(USE_API_PLAINTEXT) - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; #elif defined(USE_API_NOISE) - this->helper_ = std::unique_ptr{ - new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx(), &this->client_info_)}; + this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; #else #error "No frame helper defined" #endif @@ -131,8 +129,9 @@ void APIConnection::start() { this->fatal_error_with_log_(LOG_STR("Helper init failed"), err); return; } - this->client_info_.peername = helper_->getpeername(); - this->client_info_.name = this->client_info_.peername; + // Initialize client name with peername (IP address) until Hello message provides actual name + const char *peername = this->helper_->get_client_peername(); + this->helper_->set_client_name(peername, strlen(peername)); } APIConnection::~APIConnection() { @@ -252,8 +251,7 @@ void APIConnection::loop() { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { on_fatal_error(); - ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(), - this->client_info_.peername.c_str()); + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting")); } } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { // Only send ping if we're not disconnecting @@ -287,7 +285,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // close will happen on next loop - ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); + this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected")); this->flags_.next_close = true; DisconnectResponse resp; return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); @@ -1504,9 +1502,10 @@ void APIConnection::complete_authentication_() { } this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); - ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); + this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected")); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername); + this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()), + std::string(this->helper_->get_client_peername())); #endif #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { @@ -1521,12 +1520,12 @@ void APIConnection::complete_authentication_() { } bool APIConnection::send_hello_response(const HelloRequest &msg) { - this->client_info_.name.assign(msg.client_info.c_str(), msg.client_info.size()); - this->client_info_.peername = this->helper_->getpeername(); + // Copy client name with truncation if needed (set_client_name handles truncation) + this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size()); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; - ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name.c_str(), - this->client_info_.peername.c_str(), this->client_api_version_major_, this->client_api_version_minor_); + ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(), + this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_); HelloResponse resp; resp.api_version_major = 1; @@ -1836,7 +1835,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { } void APIConnection::on_no_setup_connection() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); + this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup")); } void APIConnection::on_fatal_error() { this->helper_->close(); @@ -2084,8 +2083,13 @@ void APIConnection::process_state_subscriptions_() { } #endif // USE_API_HOMEASSISTANT_STATES +void APIConnection::log_client_(int level, const LogString *message) { + esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(), + this->helper_->get_client_peername(), LOG_STR_ARG(message)); +} + void APIConnection::log_warning_(const LogString *message, APIError err) { - ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), this->client_info_.peername.c_str(), + ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(), LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index cffd52bfdb..802681f32f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -9,18 +9,13 @@ #include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/core/entity_base.h" +#include "esphome/core/string_ref.h" #include #include namespace esphome::api { -// Client information structure -struct ClientInfo { - std::string name; // Client name from Hello message - std::string peername; // IP:port from socket -}; - // Keepalive timeout in milliseconds static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; // Maximum number of entities to process in a single batch during initial state/info sending @@ -279,8 +274,9 @@ class APIConnection final : public APIServerConnection { bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; - const std::string &get_name() const { return this->client_info_.name; } - const std::string &get_peername() const { return this->client_info_.peername; } + const char *get_name() const { return this->helper_->get_client_name(); } + /// Get peer name (IP address) - cached at connection init time + const char *get_peername() const { return this->helper_->get_client_peername(); } protected: // Helper function to handle authentication completion @@ -526,10 +522,7 @@ class APIConnection final : public APIServerConnection { std::unique_ptr image_reader_; #endif - // Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each) - ClientInfo client_info_; - - // Group 4: 4-byte types + // Group 3: 4-byte types uint32_t last_traffic_; #ifdef USE_API_HOMEASSISTANT_STATES int state_subs_at_ = -1; @@ -756,6 +749,8 @@ class APIConnection final : public APIServerConnection { return this->schedule_batch_(); } + // Helper function to log client messages with name and peername + void log_client_(int level, const LogString *message); // Helper function to log API errors with errno void log_warning_(const LogString *message, APIError err); // Helper to handle fatal errors with logging diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 420f42a90a..dd44fe9e17 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -1,6 +1,5 @@ #include "api_frame_helper.h" #ifdef USE_API -#include "api_connection.h" // For ClientInfo struct #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -16,8 +15,11 @@ static const char *const TAG = "api.frame_helper"; // Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) static constexpr size_t API_MAX_LOG_BYTES = 168; -#define HELPER_LOG(msg, ...) \ - ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#else +#define HELPER_LOG(msg, ...) ((void) 0) +#endif #ifdef HELPER_LOG_PACKETS #define LOG_PACKET_RECEIVED(buffer) \ @@ -243,6 +245,8 @@ APIError APIFrameHelper::init_common_() { HELPER_LOG("Bad state for init %d", (int) state_); return APIError::BAD_STATE; } + // Cache peername now while socket is valid - needed for error logging after socket failure + this->socket_->getpeername_to(this->client_peername_); int err = this->socket_->setblocking(false); if (err != 0) { state_ = State::FAILED; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 383e763e6d..76a93d094e 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -33,11 +33,11 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and oth // Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there) static constexpr size_t MAX_MESSAGES_PER_BATCH = 34; -// Forward declaration -struct ClientInfo; - class ProtoWriteBuffer; +// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars) +static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32; + struct ReadPacketBuffer { const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call) uint16_t data_len; @@ -86,14 +86,23 @@ const LogString *api_error_to_logstr(APIError err); class APIFrameHelper { public: APIFrameHelper() = default; - explicit APIFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) - : socket_(std::move(socket)), client_info_(client_info) {} + explicit APIFrameHelper(std::unique_ptr socket) : socket_(std::move(socket)) {} + + // Get client name (null-terminated) + const char *get_client_name() const { return this->client_name_; } + // Get client peername/IP (null-terminated, cached at init time for availability after socket failure) + const char *get_client_peername() const { return this->client_peername_; } + // Set client name from buffer with length (truncates if needed) + void set_client_name(const char *name, size_t len) { + size_t copy_len = std::min(len, sizeof(this->client_name_) - 1); + memcpy(this->client_name_, name, copy_len); + this->client_name_[copy_len] = '\0'; + } virtual ~APIFrameHelper() = default; virtual APIError init() = 0; virtual APIError loop(); virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; } - std::string getpeername() { return socket_->getpeername(); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } APIError close() { state_ = State::CLOSED; @@ -186,9 +195,10 @@ class APIFrameHelper { std::array, API_MAX_SEND_QUEUE> tx_buf_; std::vector rx_buf_; - // Pointer to client info (4 bytes on 32-bit) - // Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance. - const ClientInfo *client_info_{nullptr}; + // Client name buffer - stores name from Hello message or initial peername + char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; + // Cached peername/IP address - captured at init time for availability after socket failure + char client_peername_[socket::SOCKADDR_STR_LEN]{}; // Group smaller types together uint16_t rx_buf_len_ = 0; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index be8d93fbf9..21b0463dfe 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -27,8 +27,11 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") // Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) static constexpr size_t API_MAX_LOG_BYTES = 168; -#define HELPER_LOG(msg, ...) \ - ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#else +#define HELPER_LOG(msg, ...) ((void) 0) +#endif #ifdef HELPER_LOG_PACKETS #define LOG_PACKET_RECEIVED(buffer) \ diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 1268086194..183b8c8a51 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -9,8 +9,8 @@ namespace esphome::api { class APINoiseFrameHelper final : public APIFrameHelper { public: - APINoiseFrameHelper(std::unique_ptr socket, APINoiseContext &ctx, const ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info), ctx_(ctx) { + APINoiseFrameHelper(std::unique_ptr socket, APINoiseContext &ctx) + : APIFrameHelper(std::move(socket)), ctx_(ctx) { // Noise header structure: // Pos 0: indicator (0x01) // Pos 1-2: encrypted payload size (16-bit big-endian) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index a974a2458e..3dfd683929 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -1,7 +1,6 @@ #include "api_frame_helper_plaintext.h" #ifdef USE_API #ifdef USE_API_PLAINTEXT -#include "api_connection.h" // For ClientInfo struct #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -21,8 +20,11 @@ static const char *const TAG = "api.plaintext"; // Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) static constexpr size_t API_MAX_LOG_BYTES = 168; -#define HELPER_LOG(msg, ...) \ - ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#else +#define HELPER_LOG(msg, ...) ((void) 0) +#endif #ifdef HELPER_LOG_PACKETS #define LOG_PACKET_RECEIVED(buffer) \ diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index 7af9fc64b9..96d47e9c7b 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -7,8 +7,7 @@ namespace esphome::api { class APIPlaintextFrameHelper final : public APIFrameHelper { public: - APIPlaintextFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info) { + explicit APIPlaintextFrameHelper(std::unique_ptr socket) : APIFrameHelper(std::move(socket)) { // Plaintext header structure (worst case): // Pos 0: indicator (0x00) // Pos 1-3: payload size varint (up to 3 bytes) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a7b046447d..4ececfec94 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -125,15 +125,18 @@ void APIServer::loop() { if (!sock) break; + char peername[socket::SOCKADDR_STR_LEN]; + sock->getpeername_to(peername); + // Check if we're at the connection limit if (this->clients_.size() >= this->max_connections_) { - ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, sock->getpeername().c_str()); + ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); // Immediately close - socket destructor will handle cleanup sock.reset(); continue; } - ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str()); + ESP_LOGD(TAG, "Accept %s", peername); auto *conn = new APIConnection(std::move(sock), this); this->clients_.emplace_back(conn); @@ -166,8 +169,7 @@ void APIServer::loop() { // Network is down - disconnect all clients for (auto &client : this->clients_) { client->on_fatal_error(); - ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(), - client->client_info_.peername.c_str()); + client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect")); } // Continue to process and clean up the clients below } @@ -185,12 +187,12 @@ void APIServer::loop() { // Rare case: handle disconnection #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername); + this->client_disconnected_trigger_->trigger(std::string(client->get_name()), std::string(client->get_peername())); #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES this->unregister_active_action_calls_for_connection(client.get()); #endif - ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str()); + ESP_LOGV(TAG, "Remove connection %s", client->get_name()); // Swap with the last element and pop (avoids expensive vector shifts) if (client_index < this->clients_.size() - 1) { diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index f71163f79e..dfa637f701 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -4,6 +4,7 @@ #include "esphome/components/sha256/sha256.h" #endif #include "esphome/components/network/util.h" +#include "esphome/components/socket/socket.h" #include "esphome/components/ota/ota_backend.h" #include "esphome/components/ota/ota_backend_esp8266.h" #include "esphome/components/ota/ota_backend_arduino_libretiny.h" @@ -443,7 +444,9 @@ void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) { void ESPHomeOTAComponent::log_read_error_(const LogString *what) { ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what)); } void ESPHomeOTAComponent::log_start_(const LogString *phase) { - ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), this->client_->getpeername().c_str()); + char peername[socket::SOCKADDR_STR_LEN]; + this->client_->getpeername_to(peername); + ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), peername); } void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) { diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index 09cd81752a..73be025376 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -14,31 +14,7 @@ namespace esphome::socket { -std::string format_sockaddr(const struct sockaddr_storage &storage) { - if (storage.ss_family == AF_INET) { - const struct sockaddr_in *addr = reinterpret_cast(&storage); - char buf[INET_ADDRSTRLEN]; - if (inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)) != nullptr) - return std::string{buf}; - } -#if LWIP_IPV6 - else if (storage.ss_family == AF_INET6) { - const struct sockaddr_in6 *addr = reinterpret_cast(&storage); - char buf[INET6_ADDRSTRLEN]; - // Format IPv4-mapped IPv6 addresses as regular IPv4 addresses - if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && - addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && - inet_ntop(AF_INET, &addr->sin6_addr.un.u32_addr[3], buf, sizeof(buf)) != nullptr) { - return std::string{buf}; - } - if (inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)) != nullptr) - return std::string{buf}; - } -#endif - return {}; -} - -class BSDSocketImpl : public Socket { +class BSDSocketImpl final : public Socket { public: BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { #ifdef USE_SOCKET_SELECT_SUPPORT @@ -93,23 +69,9 @@ class BSDSocketImpl : public Socket { int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return ::getpeername(this->fd_, addr, addrlen); } - std::string getpeername() override { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - if (::getpeername(this->fd_, (struct sockaddr *) &storage, &len) != 0) - return {}; - return format_sockaddr(storage); - } int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return ::getsockname(this->fd_, addr, addrlen); } - std::string getsockname() override { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - if (::getsockname(this->fd_, (struct sockaddr *) &storage, &len) != 0) - return {}; - return format_sockaddr(storage); - } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { return ::getsockopt(this->fd_, level, optname, optval, optlen); } diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index cb5d17d5af..429f59ceca 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -71,7 +71,7 @@ class LWIPRawImpl : public Socket { errno = EINVAL; return nullptr; } - int bind(const struct sockaddr *name, socklen_t addrlen) override { + int bind(const struct sockaddr *name, socklen_t addrlen) final { if (pcb_ == nullptr) { errno = EBADF; return -1; @@ -135,7 +135,7 @@ class LWIPRawImpl : public Socket { } return 0; } - int close() override { + int close() final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -152,7 +152,7 @@ class LWIPRawImpl : public Socket { pcb_ = nullptr; return 0; } - int shutdown(int how) override { + int shutdown(int how) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -178,7 +178,7 @@ class LWIPRawImpl : public Socket { return 0; } - int getpeername(struct sockaddr *name, socklen_t *addrlen) override { + int getpeername(struct sockaddr *name, socklen_t *addrlen) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -189,14 +189,7 @@ class LWIPRawImpl : public Socket { } return this->ip2sockaddr_(&pcb_->remote_ip, pcb_->remote_port, name, addrlen); } - std::string getpeername() override { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return ""; - } - return this->format_ip_address_(pcb_->remote_ip); - } - int getsockname(struct sockaddr *name, socklen_t *addrlen) override { + int getsockname(struct sockaddr *name, socklen_t *addrlen) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -207,14 +200,7 @@ class LWIPRawImpl : public Socket { } return this->ip2sockaddr_(&pcb_->local_ip, pcb_->local_port, name, addrlen); } - std::string getsockname() override { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return ""; - } - return this->format_ip_address_(pcb_->local_ip); - } - int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { + int getsockopt(int level, int optname, void *optval, socklen_t *optlen) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -248,7 +234,7 @@ class LWIPRawImpl : public Socket { errno = EINVAL; return -1; } - int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { + int setsockopt(int level, int optname, const void *optval, socklen_t optlen) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -282,7 +268,7 @@ class LWIPRawImpl : public Socket { errno = EOPNOTSUPP; return -1; } - ssize_t read(void *buf, size_t len) override { + ssize_t read(void *buf, size_t len) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -340,7 +326,7 @@ class LWIPRawImpl : public Socket { return read; } - ssize_t readv(const struct iovec *iov, int iovcnt) override { + ssize_t readv(const struct iovec *iov, int iovcnt) final { ssize_t ret = 0; for (int i = 0; i < iovcnt; i++) { ssize_t err = read(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); @@ -358,7 +344,7 @@ class LWIPRawImpl : public Socket { return ret; } - ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override { + ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) final { errno = ENOTSUP; return -1; } @@ -412,7 +398,7 @@ class LWIPRawImpl : public Socket { } return 0; } - ssize_t write(const void *buf, size_t len) override { + ssize_t write(const void *buf, size_t len) final { ssize_t written = internal_write(buf, len); if (written == -1) return -1; @@ -427,7 +413,7 @@ class LWIPRawImpl : public Socket { } return written; } - ssize_t writev(const struct iovec *iov, int iovcnt) override { + ssize_t writev(const struct iovec *iov, int iovcnt) final { ssize_t written = 0; for (int i = 0; i < iovcnt; i++) { ssize_t err = internal_write(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); @@ -453,12 +439,12 @@ class LWIPRawImpl : public Socket { } return written; } - ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { + ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) final { // return ::sendto(fd_, buf, len, flags, to, tolen); errno = ENOSYS; return -1; } - int setblocking(bool blocking) override { + int setblocking(bool blocking) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -517,19 +503,6 @@ class LWIPRawImpl : public Socket { } protected: - std::string format_ip_address_(const ip_addr_t &ip) { - char buffer[50] = {}; - if (IP_IS_V4_VAL(ip)) { - inet_ntoa_r(ip, buffer, sizeof(buffer)); - } -#if LWIP_IPV6 - else if (IP_IS_V6_VAL(ip)) { - inet6_ntoa_r(ip, buffer, sizeof(buffer)); - } -#endif - return std::string(buffer); - } - int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) { if (family_ == AF_INET) { if (*addrlen < sizeof(struct sockaddr_in)) { @@ -584,7 +557,7 @@ class LWIPRawImpl : public Socket { // Listening socket class - only allocates accept queue when needed (for bind+listen sockets) // This saves 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) for regular connected sockets on ESP8266/RP2040 -class LWIPRawListenImpl : public LWIPRawImpl { +class LWIPRawListenImpl final : public LWIPRawImpl { public: LWIPRawListenImpl(sa_family_t family, struct tcp_pcb *pcb) : LWIPRawImpl(family, pcb) {} diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index 23fb1a7f6f..a885f243f3 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -9,29 +9,7 @@ namespace esphome::socket { -std::string format_sockaddr(const struct sockaddr_storage &storage) { - if (storage.ss_family == AF_INET) { - const struct sockaddr_in *addr = reinterpret_cast(&storage); - char buf[INET_ADDRSTRLEN]; - const char *ret = lwip_inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)); - if (ret == nullptr) - return {}; - return std::string{buf}; - } -#if LWIP_IPV6 - else if (storage.ss_family == AF_INET6) { - const struct sockaddr_in6 *addr = reinterpret_cast(&storage); - char buf[INET6_ADDRSTRLEN]; - const char *ret = lwip_inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)); - if (ret == nullptr) - return {}; - return std::string{buf}; - } -#endif - return {}; -} - -class LwIPSocketImpl : public Socket { +class LwIPSocketImpl final : public Socket { public: LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { #ifdef USE_SOCKET_SELECT_SUPPORT @@ -88,23 +66,9 @@ class LwIPSocketImpl : public Socket { int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return lwip_getpeername(this->fd_, addr, addrlen); } - std::string getpeername() override { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - if (lwip_getpeername(this->fd_, (struct sockaddr *) &storage, &len) != 0) - return {}; - return format_sockaddr(storage); - } int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return lwip_getsockname(this->fd_, addr, addrlen); } - std::string getsockname() override { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - if (lwip_getsockname(this->fd_, (struct sockaddr *) &storage, &len) != 0) - return {}; - return format_sockaddr(storage); - } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { return lwip_getsockopt(this->fd_, level, optname, optval, optlen); } diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index ffe0233abc..c92e33393b 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -10,6 +10,87 @@ namespace esphome::socket { Socket::~Socket() {} +// Platform-specific inet_ntop wrappers +#if defined(USE_SOCKET_IMPL_LWIP_TCP) +// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value +static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) { + inet_ntoa_r(*reinterpret_cast(addr), buf, size); + return buf; +} +#if USE_NETWORK_IPV6 +static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) { + inet6_ntoa_r(*reinterpret_cast(addr), buf, size); + return buf; +} +#endif +#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS) +// LWIP sockets (LibreTiny, ESP32 Arduino) +static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) { + return lwip_inet_ntop(AF_INET, addr, buf, size); +} +#if USE_NETWORK_IPV6 +static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) { + return lwip_inet_ntop(AF_INET6, addr, buf, size); +} +#endif +#else +// BSD sockets (host, ESP32-IDF) +static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) { + return inet_ntop(AF_INET, addr, buf, size); +} +#if USE_NETWORK_IPV6 +static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) { + return inet_ntop(AF_INET6, addr, buf, size); +} +#endif +#endif + +// Format sockaddr into caller-provided buffer, returns length written (excluding null) +static size_t format_sockaddr_to(const struct sockaddr_storage &storage, std::span buf) { + if (storage.ss_family == AF_INET) { + const auto *addr = reinterpret_cast(&storage); + if (esphome_inet_ntop4(&addr->sin_addr, buf.data(), buf.size()) != nullptr) + return strlen(buf.data()); + } +#if USE_NETWORK_IPV6 + else if (storage.ss_family == AF_INET6) { + const auto *addr = reinterpret_cast(&storage); +#ifndef USE_SOCKET_IMPL_LWIP_TCP + // Format IPv4-mapped IPv6 addresses as regular IPv4 (not supported on ESP8266 raw TCP) + if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && + addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && + esphome_inet_ntop4(&addr->sin6_addr.un.u32_addr[3], buf.data(), buf.size()) != nullptr) { + return strlen(buf.data()); + } +#endif + if (esphome_inet_ntop6(&addr->sin6_addr, buf.data(), buf.size()) != nullptr) + return strlen(buf.data()); + } +#endif + buf[0] = '\0'; + return 0; +} + +size_t Socket::getpeername_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getpeername(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; + return 0; + } + return format_sockaddr_to(storage, buf); +} + +size_t Socket::getsockname_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getsockname(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; + return 0; + } + return format_sockaddr_to(storage, buf); +} + std::unique_ptr socket_ip(int type, int protocol) { #if USE_NETWORK_IPV6 return socket(AF_INET6, type, protocol); diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 75eb07de4a..9f9f61de85 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include "esphome/core/optional.h" @@ -8,6 +9,15 @@ #if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) namespace esphome::socket { +// Maximum length for formatted socket address string (IP address without port) +// IPv4: "255.255.255.255" = 15 chars + null = 16 +// IPv6: full address = 45 chars + null = 46 +#if USE_NETWORK_IPV6 +static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN +#else +static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN +#endif + class Socket { public: Socket() = default; @@ -31,9 +41,15 @@ class Socket { virtual int shutdown(int how) = 0; virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; - virtual std::string getpeername() = 0; virtual int getsockname(struct sockaddr *addr, socklen_t *addrlen) = 0; - virtual std::string getsockname() = 0; + + /// Format peer address into a fixed-size buffer (no heap allocation) + /// Non-virtual wrapper around getpeername() - can be optimized away if unused + /// Returns number of characters written (excluding null terminator), or 0 on error + size_t getpeername_to(std::span buf); + /// Format local address into a fixed-size buffer (no heap allocation) + /// Non-virtual wrapper around getsockname() - can be optimized away if unused + size_t getsockname_to(std::span buf); virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0; virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0; virtual int listen(int backlog) = 0; diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 05c356ae4c..0e0616c508 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -3,6 +3,7 @@ #ifdef USE_VOICE_ASSISTANT +#include "esphome/components/socket/socket.h" #include "esphome/core/log.h" #include @@ -433,8 +434,8 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr "Multiple API Clients attempting to connect to Voice Assistant\n" "Current client: %s (%s)\n" "New client: %s (%s)", - this->api_client_->get_name().c_str(), this->api_client_->get_peername().c_str(), - client->get_name().c_str(), client->get_peername().c_str()); + this->api_client_->get_name(), this->api_client_->get_peername(), client->get_name(), + client->get_peername()); return; }