Merge pull request #12444 from esphome/bump-2025.12.0b2

2025.12.0b2
This commit is contained in:
Jonathan Swoboda
2025-12-12 12:15:41 -05:00
committed by GitHub
14 changed files with 115 additions and 37 deletions

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2025.12.0b1 PROJECT_NUMBER = 2025.12.0b2
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a

View File

@@ -579,7 +579,7 @@ message LightCommandRequest {
bool has_flash_length = 16; bool has_flash_length = 16;
uint32 flash_length = 17; uint32 flash_length = 17;
bool has_effect = 18; bool has_effect = 18;
string effect = 19; string effect = 19 [(pointer_to_buffer) = true];
uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"];
} }

View File

@@ -533,7 +533,7 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
if (msg.has_flash_length) if (msg.has_flash_length)
call.set_flash_length(msg.flash_length); call.set_flash_length(msg.flash_length);
if (msg.has_effect) if (msg.has_effect)
call.set_effect(msg.effect); call.set_effect(reinterpret_cast<const char *>(msg.effect), msg.effect_len);
call.perform(); call.perform();
} }
#endif #endif
@@ -1669,7 +1669,7 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
} else { } else {
ESP_LOGW(TAG, "Failed to clear encryption key"); ESP_LOGW(TAG, "Failed to clear encryption key");
} }
} else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { } else if (base64_decode(msg.key, psk.data(), psk.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length"); ESP_LOGW(TAG, "Invalid encryption key length");
} else if (!this->parent_->save_noise_psk(psk, true)) { } else if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key"); ESP_LOGW(TAG, "Failed to save encryption key");

View File

@@ -611,9 +611,12 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
} }
bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 19: case 19: {
this->effect = value.as_string(); // Use raw data directly to avoid allocation
this->effect = value.data();
this->effect_len = value.size();
break; break;
}
default: default:
return false; return false;
} }

View File

@@ -840,7 +840,7 @@ class LightStateResponse final : public StateResponseProtoMessage {
class LightCommandRequest final : public CommandProtoMessage { class LightCommandRequest final : public CommandProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 32; static constexpr uint8_t MESSAGE_TYPE = 32;
static constexpr uint8_t ESTIMATED_SIZE = 112; static constexpr uint8_t ESTIMATED_SIZE = 122;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "light_command_request"; } const char *message_name() const override { return "light_command_request"; }
#endif #endif
@@ -869,7 +869,8 @@ class LightCommandRequest final : public CommandProtoMessage {
bool has_flash_length{false}; bool has_flash_length{false};
uint32_t flash_length{0}; uint32_t flash_length{0};
bool has_effect{false}; bool has_effect{false};
std::string effect{}; const uint8_t *effect{nullptr};
uint16_t effect_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif

View File

@@ -999,7 +999,9 @@ void LightCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_flash_length", this->has_flash_length); dump_field(out, "has_flash_length", this->has_flash_length);
dump_field(out, "flash_length", this->flash_length); dump_field(out, "flash_length", this->flash_length);
dump_field(out, "has_effect", this->has_effect); dump_field(out, "has_effect", this->has_effect);
dump_field(out, "effect", this->effect); out.append(" effect: ");
out.append(format_hex_pretty(this->effect, this->effect_len));
out.append("\n");
#ifdef USE_DEVICES #ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id); dump_field(out, "device_id", this->device_id);
#endif #endif

View File

@@ -36,6 +36,10 @@ void HttpRequestUpdate::setup() {
} }
void HttpRequestUpdate::update() { void HttpRequestUpdate::update() {
if (!network::is_connected()) {
ESP_LOGD(TAG, "Network not connected, skipping update check");
return;
}
#ifdef USE_ESP32 #ifdef USE_ESP32
xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_);
#else #else

View File

@@ -504,8 +504,8 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
#undef KEY #undef KEY
} }
LightCall &LightCall::set_effect(const std::string &effect) { LightCall &LightCall::set_effect(const char *effect, size_t len) {
if (strcasecmp(effect.c_str(), "none") == 0) { if (len == 4 && strncasecmp(effect, "none", 4) == 0) {
this->set_effect(0); this->set_effect(0);
return *this; return *this;
} }
@@ -513,15 +513,16 @@ LightCall &LightCall::set_effect(const std::string &effect) {
bool found = false; bool found = false;
for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) { for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) {
LightEffect *e = this->parent_->effects_[i]; LightEffect *e = this->parent_->effects_[i];
const char *name = e->get_name();
if (strcasecmp(effect.c_str(), e->get_name()) == 0) { if (strncasecmp(effect, name, len) == 0 && name[len] == '\0') {
this->set_effect(i + 1); this->set_effect(i + 1);
found = true; found = true;
break; break;
} }
} }
if (!found) { if (!found) {
ESP_LOGW(TAG, "'%s': no such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); ESP_LOGW(TAG, "'%s': no such effect '%.*s'", this->parent_->get_name().c_str(), (int) len, effect);
} }
return *this; return *this;
} }

View File

@@ -129,7 +129,9 @@ class LightCall {
/// Set the effect of the light by its name. /// Set the effect of the light by its name.
LightCall &set_effect(optional<std::string> effect); LightCall &set_effect(optional<std::string> effect);
/// Set the effect of the light by its name. /// Set the effect of the light by its name.
LightCall &set_effect(const std::string &effect); LightCall &set_effect(const std::string &effect) { return this->set_effect(effect.data(), effect.size()); }
/// Set the effect of the light by its name and length (zero-copy from API).
LightCall &set_effect(const char *effect, size_t len);
/// Set the effect of the light by its internal index number (only for internal use). /// Set the effect of the light by its internal index number (only for internal use).
LightCall &set_effect(uint32_t effect_number); LightCall &set_effect(uint32_t effect_number);
LightCall &set_effect(optional<uint32_t> effect_number); LightCall &set_effect(optional<uint32_t> effect_number);

View File

@@ -14,13 +14,36 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef USE_ESP8266
#include <coredecls.h> // For esp_schedule()
#endif
namespace esphome { namespace esphome {
namespace socket { namespace socket {
#ifdef USE_ESP8266
// Flag to signal socket activity - checked by socket_delay() to exit early
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static volatile bool s_socket_woke = false;
void socket_delay(uint32_t ms) {
// Use esp_delay with a callback that checks if socket data arrived.
// This allows the delay to exit early when socket_wake() is called by
// lwip recv_fn/accept_fn callbacks, reducing socket latency.
s_socket_woke = false;
esp_delay(ms, []() { return !s_socket_woke; });
}
void socket_wake() {
s_socket_woke = true;
esp_schedule();
}
#endif
static const char *const TAG = "socket.lwip"; static const char *const TAG = "socket.lwip";
// set to 1 to enable verbose lwip logging // set to 1 to enable verbose lwip logging
#if 0 #if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if)
#define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__) #define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__)
#else #else
#define LWIP_LOG(msg, ...) #define LWIP_LOG(msg, ...)
@@ -323,9 +346,10 @@ class LWIPRawImpl : public Socket {
for (int i = 0; i < iovcnt; i++) { for (int i = 0; i < iovcnt; i++) {
ssize_t err = read(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); ssize_t err = read(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
if (err == -1) { if (err == -1) {
if (ret != 0) if (ret != 0) {
// if we already read some don't return an error // if we already read some don't return an error
break; break;
}
return err; return err;
} }
ret += err; ret += err;
@@ -393,9 +417,10 @@ class LWIPRawImpl : public Socket {
ssize_t written = internal_write(buf, len); ssize_t written = internal_write(buf, len);
if (written == -1) if (written == -1)
return -1; return -1;
if (written == 0) if (written == 0) {
// no need to output if nothing written // no need to output if nothing written
return 0; return 0;
}
if (nodelay_) { if (nodelay_) {
int err = internal_output(); int err = internal_output();
if (err == -1) if (err == -1)
@@ -408,18 +433,20 @@ class LWIPRawImpl : public Socket {
for (int i = 0; i < iovcnt; i++) { for (int i = 0; i < iovcnt; i++) {
ssize_t err = internal_write(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); ssize_t err = internal_write(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
if (err == -1) { if (err == -1) {
if (written != 0) if (written != 0) {
// if we already read some don't return an error // if we already read some don't return an error
break; break;
}
return err; return err;
} }
written += err; written += err;
if ((size_t) err != iov[i].iov_len) if ((size_t) err != iov[i].iov_len)
break; break;
} }
if (written == 0) if (written == 0) {
// no need to output if nothing written // no need to output if nothing written
return 0; return 0;
}
if (nodelay_) { if (nodelay_) {
int err = internal_output(); int err = internal_output();
if (err == -1) if (err == -1)
@@ -473,6 +500,10 @@ class LWIPRawImpl : public Socket {
} else { } else {
pbuf_cat(rx_buf_, pb); pbuf_cat(rx_buf_, pb);
} }
#ifdef USE_ESP8266
// Wake the main loop immediately so it can process the received data.
socket_wake();
#endif
return ERR_OK; return ERR_OK;
} }
@@ -612,7 +643,7 @@ class LWIPRawListenImpl : public LWIPRawImpl {
} }
private: private:
err_t accept_fn(struct tcp_pcb *newpcb, err_t err) { err_t accept_fn_(struct tcp_pcb *newpcb, err_t err) {
LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err);
if (err != ERR_OK || newpcb == nullptr) { if (err != ERR_OK || newpcb == nullptr) {
// "An error code if there has been an error accepting. Only return ERR_ABRT if you have // "An error code if there has been an error accepting. Only return ERR_ABRT if you have
@@ -633,12 +664,16 @@ class LWIPRawListenImpl : public LWIPRawImpl {
sock->init(); sock->init();
accepted_sockets_[accepted_socket_count_++] = std::move(sock); accepted_sockets_[accepted_socket_count_++] = std::move(sock);
LWIP_LOG("Accepted connection, queue size: %d", accepted_socket_count_); LWIP_LOG("Accepted connection, queue size: %d", accepted_socket_count_);
#ifdef USE_ESP8266
// Wake the main loop immediately so it can accept the new connection.
socket_wake();
#endif
return ERR_OK; return ERR_OK;
} }
static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) {
LWIPRawListenImpl *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg); LWIPRawListenImpl *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg);
return arg_this->accept_fn(newpcb, err); return arg_this->accept_fn_(newpcb, err);
} }
// Accept queue - holds incoming connections briefly until the event loop calls accept() // Accept queue - holds incoming connections briefly until the event loop calls accept()

View File

@@ -82,6 +82,15 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri
/// Set a sockaddr to the any address and specified port for the IP version used by socket_ip(). /// Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port); socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port);
#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)
/// Delay that can be woken early by socket activity.
/// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay.
void socket_delay(uint32_t ms);
/// Called by lwip callbacks to signal socket activity and wake delay.
void socket_wake();
#endif
} // namespace socket } // namespace socket
} // namespace esphome } // namespace esphome
#endif #endif

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum from esphome.enum import StrEnum
__version__ = "2025.12.0b1" __version__ = "2025.12.0b2"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = ( VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -12,6 +12,10 @@
#include "esphome/components/status_led/status_led.h" #include "esphome/components/status_led/status_led.h"
#endif #endif
#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)
#include "esphome/components/socket/socket.h"
#endif
#ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_SOCKET_SELECT_SUPPORT
#include <cerrno> #include <cerrno>
@@ -627,6 +631,9 @@ void Application::yield_with_select_(uint32_t delay_ms) {
// No sockets registered, use regular delay // No sockets registered, use regular delay
delay(delay_ms); delay(delay_ms);
} }
#elif defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)
// No select support but can wake on socket activity via esp_schedule()
socket::socket_delay(delay_ms);
#else #else
// No select support, use regular delay // No select support, use regular delay
delay(delay_ms); delay(delay_ms);

View File

@@ -480,22 +480,13 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) {
} }
size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len) { size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len) {
std::vector<uint8_t> decoded = base64_decode(encoded_string);
if (decoded.size() > buf_len) {
ESP_LOGW(TAG, "Base64 decode: buffer too small, truncating");
decoded.resize(buf_len);
}
memcpy(buf, decoded.data(), decoded.size());
return decoded.size();
}
std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
int in_len = encoded_string.size(); int in_len = encoded_string.size();
int i = 0; int i = 0;
int j = 0; int j = 0;
int in = 0; int in = 0;
size_t out = 0;
uint8_t char_array_4[4], char_array_3[3]; uint8_t char_array_4[4], char_array_3[3];
std::vector<uint8_t> ret; bool truncated = false;
// SAFETY: The loop condition checks is_base64() before processing each character. // SAFETY: The loop condition checks is_base64() before processing each character.
// This ensures base64_find_char() is only called on valid base64 characters, // This ensures base64_find_char() is only called on valid base64 characters,
@@ -511,8 +502,13 @@ std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (i = 0; (i < 3); i++) for (i = 0; i < 3; i++) {
ret.push_back(char_array_3[i]); if (out < buf_len) {
buf[out++] = char_array_3[i];
} else {
truncated = true;
}
}
i = 0; i = 0;
} }
} }
@@ -528,10 +524,28 @@ std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (j = 0; (j < i - 1); j++) for (j = 0; j < i - 1; j++) {
ret.push_back(char_array_3[j]); if (out < buf_len) {
buf[out++] = char_array_3[j];
} else {
truncated = true;
}
}
} }
if (truncated) {
ESP_LOGW(TAG, "Base64 decode: buffer too small, truncating");
}
return out;
}
std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
// Calculate maximum decoded size: every 4 base64 chars = 3 bytes
size_t max_len = ((encoded_string.size() + 3) / 4) * 3;
std::vector<uint8_t> ret(max_len);
size_t actual_len = base64_decode(encoded_string, ret.data(), max_len);
ret.resize(actual_len);
return ret; return ret;
} }