Compare commits

..

24 Commits

Author SHA1 Message Date
J. Nick Koston
58659e4893 [mdns] Throttle MDNS.update() polling on ESP8266 and RP2040 (#13917) 2026-02-10 18:48:13 -06:00
Jonathan Swoboda
b4707344d3 [esp32] Upgrade uv to 0.10.1 and increase HTTP retries (#13918)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 00:44:12 +00:00
Jonathan Swoboda
548b7e5dab [esp32] Fix ESP32-P4 test: replace stale esp_hosted component ref (#13920)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 00:04:12 +00:00
Jesse Hills
b9c2be8228 Merge branch 'release' into dev 2026-02-11 11:13:33 +13:00
Jesse Hills
fb2f0ce62f Merge pull request #13915 from esphome/bump-2026.1.5
2026.1.5
2026-02-11 11:13:08 +13:00
J. Nick Koston
d152438335 [libretiny] Update LibreTiny to v1.12.1 (#13851) 2026-02-10 20:07:09 +00:00
J. Nick Koston
868a2151e3 [web_server_idf] Reduce heap allocations by using stack buffers (#13549) 2026-02-10 13:56:12 -06:00
J. Nick Koston
c65d3a0072 [mqtt] Add zero-allocation topic getters to MQTT_COMPONENT_CUSTOM_TOPIC macro (#13811) 2026-02-10 13:55:16 -06:00
J. Nick Koston
e2fad9a6c9 [sprinkler] Convert state and request origin strings to PROGMEM_STRING_TABLE (#13806) 2026-02-10 13:55:01 -06:00
J. Nick Koston
5365faa877 [debug] Move ESP8266 switch tables to flash with PROGMEM_STRING_TABLE (#13813) 2026-02-10 13:54:48 -06:00
J. Nick Koston
86feb4e27a [rtttl] Convert state_to_string to PROGMEM_STRING_TABLE (#13807)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-10 13:54:37 -06:00
J. Nick Koston
2a6d9d6325 [mqtt] Avoid heap allocation in on_log by using const char* publish overload (#13809) 2026-02-10 13:54:22 -06:00
J. Nick Koston
727bb27611 [bmp3xx_base/bmp581_base] Convert oversampling and IIR filter strings to PROGMEM_STRING_TABLE (#13808) 2026-02-10 13:54:07 -06:00
J. Nick Koston
c03abcdb86 [http_request] Reduce heap allocations in update check by parsing JSON directly from buffer (#13588) 2026-02-10 13:53:53 -06:00
Jesse Hills
a99f75ca71 Bump version to 2026.1.5 2026-02-11 08:45:06 +13:00
Sean Kelly
4168e8c30d [aqi] Fix AQI calculation for specific pm2.5 or pm10 readings (#13770) 2026-02-11 08:45:06 +13:00
J. Nick Koston
1a6c67f92e [ssd1306_base] Move switch tables to PROGMEM with lookup tables (#13814) 2026-02-10 13:45:03 -06:00
Jonathan Swoboda
1f761902b6 [esp32] Set UV_CACHE_DIR inside data dir so Clean All clears it (#13888)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 07:48:20 +13:00
Clyde Stubbs
0b047c334d [lvgl] Fix crash with unconfigured top_layer (#13846) 2026-02-11 07:24:32 +13:00
tomaszduda23
a5dc4b0fce [nrf52,logger] fix printk (#13874) 2026-02-11 07:24:32 +13:00
J. Nick Koston
c1455ccc29 [dashboard] Close WebSocket after process exit to prevent zombie connections (#13834) 2026-02-11 07:24:32 +13:00
Jonathan Swoboda
438a0c4289 [ota] Fix CLI upload option shown when only http_request platform configured (#13784)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 07:24:32 +13:00
Jonathan Swoboda
9eee4c9924 [core] Add capacity check to register_component_ (#13778)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 07:24:32 +13:00
Jas Strong
eea7e9edff [rd03d] Revert incorrect field order swap (#13769)
Co-authored-by: jas <jas@asspa.in>
2026-02-11 07:24:32 +13:00
34 changed files with 777 additions and 493 deletions

View File

@@ -1 +1 @@
8dc4dae0acfa22f26c7cde87fc24e60b27f29a73300e02189b78f0315e5d0695 74867fc82764102ce1275ea2bc43e3aeee7619679537c6db61114a33342bb4c7

View File

@@ -23,7 +23,7 @@ RUN if command -v apk > /dev/null; then \
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 ENV PIP_DISABLE_PIP_VERSION_CHECK=1
RUN pip install --no-cache-dir -U pip uv==0.6.14 RUN pip install --no-cache-dir -U pip uv==0.10.1
COPY requirements.txt / COPY requirements.txt /

View File

@@ -117,7 +117,37 @@ void APIServer::setup() {
void APIServer::loop() { void APIServer::loop() {
// Accept new clients only if the socket exists and has incoming connections // Accept new clients only if the socket exists and has incoming connections
if (this->socket_ && this->socket_->ready()) { if (this->socket_ && this->socket_->ready()) {
this->accept_new_connections_(); while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
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_, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
}
ESP_LOGD(TAG, "Accept %s", peername);
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
// First client connected - clear warning and update timestamp
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
}
} }
if (this->clients_.empty()) { if (this->clients_.empty()) {
@@ -148,84 +178,46 @@ void APIServer::loop() {
while (client_index < this->clients_.size()) { while (client_index < this->clients_.size()) {
auto &client = this->clients_[client_index]; auto &client = this->clients_[client_index];
if (client->flags_.remove) { if (!client->flags_.remove) {
// Rare case: handle disconnection (don't increment - swapped element needs processing)
this->remove_client_(client_index);
} else {
// Common case: process active client // Common case: process active client
client->loop(); client->loop();
client_index++; client_index++;
}
}
}
void APIServer::remove_client_(size_t client_index) {
auto &client = this->clients_[client_index];
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before closing socket and removal for the trigger
char peername_buf[socket::SOCKADDR_STR_LEN];
std::string client_name(client->get_name());
std::string client_peername(client->get_peername_to(peername_buf));
#endif
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_.trigger(client_name, client_peername);
#endif
}
void APIServer::accept_new_connections_() {
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
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_, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue; continue;
} }
ESP_LOGD(TAG, "Accept %s", peername); // Rare case: handle disconnection
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
auto *conn = new APIConnection(std::move(sock), this); #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
this->clients_.emplace_back(conn); // Save client info before closing socket and removal for the trigger
conn->start(); char peername_buf[socket::SOCKADDR_STR_LEN];
std::string client_name(client->get_name());
std::string client_peername(client->get_peername_to(peername_buf));
#endif
// First client connected - clear warning and update timestamp // Close socket now (was deferred from on_fatal_error to allow getpeername)
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { client->helper_->close();
this->status_clear_warning();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time(); this->last_connected_ = App.get_loop_component_start_time();
} }
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_.trigger(client_name, client_peername);
#endif
// Don't increment client_index since we need to process the swapped element
} }
} }

View File

@@ -234,11 +234,6 @@ class APIServer : public Component,
#endif #endif
protected: protected:
// Accept incoming socket connections. Only called when socket has pending connections.
void __attribute__((noinline)) accept_new_connections_();
// Remove a disconnected client by index. Swaps with last element and pops.
void __attribute__((noinline)) remove_client_(size_t client_index);
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
const psk_t &active_psk, bool make_active); const psk_t &active_psk, bool make_active);

View File

@@ -159,6 +159,10 @@ BK72XX_BOARD_PINS = {
"A0": 23, "A0": 23,
}, },
"cbu": { "cbu": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20, "WIRE1_SCL": 20,
"WIRE1_SDA": 21, "WIRE1_SDA": 21,
"WIRE2_SCL": 0, "WIRE2_SCL": 0,
@@ -227,6 +231,10 @@ BK72XX_BOARD_PINS = {
"A0": 23, "A0": 23,
}, },
"generic-bk7231t-qfn32-tuya": { "generic-bk7231t-qfn32-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20, "WIRE1_SCL": 20,
"WIRE1_SDA": 21, "WIRE1_SDA": 21,
"WIRE2_SCL": 0, "WIRE2_SCL": 0,
@@ -295,6 +303,10 @@ BK72XX_BOARD_PINS = {
"A0": 23, "A0": 23,
}, },
"generic-bk7231n-qfn32-tuya": { "generic-bk7231n-qfn32-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20, "WIRE1_SCL": 20,
"WIRE1_SDA": 21, "WIRE1_SDA": 21,
"WIRE2_SCL": 0, "WIRE2_SCL": 0,
@@ -485,8 +497,7 @@ BK72XX_BOARD_PINS = {
}, },
"cb3s": { "cb3s": {
"WIRE1_SCL": 20, "WIRE1_SCL": 20,
"WIRE1_SDA_0": 21, "WIRE1_SDA": 21,
"WIRE1_SDA_1": 21,
"SERIAL1_RX": 10, "SERIAL1_RX": 10,
"SERIAL1_TX": 11, "SERIAL1_TX": 11,
"SERIAL2_TX": 0, "SERIAL2_TX": 0,
@@ -647,6 +658,10 @@ BK72XX_BOARD_PINS = {
"A0": 23, "A0": 23,
}, },
"generic-bk7252": { "generic-bk7252": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20, "WIRE1_SCL": 20,
"WIRE1_SDA": 21, "WIRE1_SDA": 21,
"WIRE2_SCL": 0, "WIRE2_SCL": 0,
@@ -1096,6 +1111,10 @@ BK72XX_BOARD_PINS = {
"A0": 23, "A0": 23,
}, },
"cb3se": { "cb3se": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL": 0, "WIRE2_SCL": 0,
"WIRE2_SDA": 1, "WIRE2_SDA": 1,
"SERIAL1_RX": 10, "SERIAL1_RX": 10,

View File

@@ -6,8 +6,9 @@
*/ */
#include "bmp3xx_base.h" #include "bmp3xx_base.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cinttypes> #include <cinttypes>
namespace esphome { namespace esphome {
@@ -26,46 +27,18 @@ static const LogString *chip_type_to_str(uint8_t chip_type) {
} }
} }
// Oversampling strings indexed by Oversampling enum (0-5): NONE, X2, X4, X8, X16, X32
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "");
static const LogString *oversampling_to_str(Oversampling oversampling) { static const LogString *oversampling_to_str(Oversampling oversampling) {
switch (oversampling) { return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
case Oversampling::OVERSAMPLING_NONE:
return LOG_STR("None");
case Oversampling::OVERSAMPLING_X2:
return LOG_STR("2x");
case Oversampling::OVERSAMPLING_X4:
return LOG_STR("4x");
case Oversampling::OVERSAMPLING_X8:
return LOG_STR("8x");
case Oversampling::OVERSAMPLING_X16:
return LOG_STR("16x");
case Oversampling::OVERSAMPLING_X32:
return LOG_STR("32x");
default:
return LOG_STR("");
}
} }
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *iir_filter_to_str(IIRFilter filter) { static const LogString *iir_filter_to_str(IIRFilter filter) {
switch (filter) { return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
case IIRFilter::IIR_FILTER_OFF:
return LOG_STR("OFF");
case IIRFilter::IIR_FILTER_2:
return LOG_STR("2x");
case IIRFilter::IIR_FILTER_4:
return LOG_STR("4x");
case IIRFilter::IIR_FILTER_8:
return LOG_STR("8x");
case IIRFilter::IIR_FILTER_16:
return LOG_STR("16x");
case IIRFilter::IIR_FILTER_32:
return LOG_STR("32x");
case IIRFilter::IIR_FILTER_64:
return LOG_STR("64x");
case IIRFilter::IIR_FILTER_128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
} }
void BMP3XXComponent::setup() { void BMP3XXComponent::setup() {

View File

@@ -11,57 +11,26 @@
*/ */
#include "bmp581_base.h" #include "bmp581_base.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::bmp581_base { namespace esphome::bmp581_base {
static const char *const TAG = "bmp581"; static const char *const TAG = "bmp581";
// Oversampling strings indexed by Oversampling enum (0-7): NONE, X2, X4, X8, X16, X32, X64, X128
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *oversampling_to_str(Oversampling oversampling) { static const LogString *oversampling_to_str(Oversampling oversampling) {
switch (oversampling) { return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
case Oversampling::OVERSAMPLING_NONE:
return LOG_STR("None");
case Oversampling::OVERSAMPLING_X2:
return LOG_STR("2x");
case Oversampling::OVERSAMPLING_X4:
return LOG_STR("4x");
case Oversampling::OVERSAMPLING_X8:
return LOG_STR("8x");
case Oversampling::OVERSAMPLING_X16:
return LOG_STR("16x");
case Oversampling::OVERSAMPLING_X32:
return LOG_STR("32x");
case Oversampling::OVERSAMPLING_X64:
return LOG_STR("64x");
case Oversampling::OVERSAMPLING_X128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
} }
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *iir_filter_to_str(IIRFilter filter) { static const LogString *iir_filter_to_str(IIRFilter filter) {
switch (filter) { return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
case IIRFilter::IIR_FILTER_OFF:
return LOG_STR("OFF");
case IIRFilter::IIR_FILTER_2:
return LOG_STR("2x");
case IIRFilter::IIR_FILTER_4:
return LOG_STR("4x");
case IIRFilter::IIR_FILTER_8:
return LOG_STR("8x");
case IIRFilter::IIR_FILTER_16:
return LOG_STR("16x");
case IIRFilter::IIR_FILTER_32:
return LOG_STR("32x");
case IIRFilter::IIR_FILTER_64:
return LOG_STR("64x");
case IIRFilter::IIR_FILTER_128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
} }
void BMP581Component::dump_config() { void BMP581Component::dump_config() {

View File

@@ -1,6 +1,7 @@
#include "debug_component.h" #include "debug_component.h"
#ifdef USE_ESP8266 #ifdef USE_ESP8266
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <Esp.h> #include <Esp.h>
extern "C" { extern "C" {
@@ -19,27 +20,38 @@ namespace debug {
static const char *const TAG = "debug"; static const char *const TAG = "debug";
// PROGMEM string table for reset reasons, indexed by reason code (0-6), with "Unknown" as fallback
// clang-format off
PROGMEM_STRING_TABLE(ResetReasonStrings,
"Power On", // 0 = REASON_DEFAULT_RST
"Hardware Watchdog", // 1 = REASON_WDT_RST
"Exception", // 2 = REASON_EXCEPTION_RST
"Software Watchdog", // 3 = REASON_SOFT_WDT_RST
"Software/System restart", // 4 = REASON_SOFT_RESTART
"Deep-Sleep Wake", // 5 = REASON_DEEP_SLEEP_AWAKE
"External System", // 6 = REASON_EXT_SYS_RST
"Unknown" // 7 = fallback
);
// clang-format on
static_assert(REASON_DEFAULT_RST == 0, "Reset reason enum values must match table indices");
static_assert(REASON_WDT_RST == 1, "Reset reason enum values must match table indices");
static_assert(REASON_EXCEPTION_RST == 2, "Reset reason enum values must match table indices");
static_assert(REASON_SOFT_WDT_RST == 3, "Reset reason enum values must match table indices");
static_assert(REASON_SOFT_RESTART == 4, "Reset reason enum values must match table indices");
static_assert(REASON_DEEP_SLEEP_AWAKE == 5, "Reset reason enum values must match table indices");
static_assert(REASON_EXT_SYS_RST == 6, "Reset reason enum values must match table indices");
// PROGMEM string table for flash chip modes, indexed by mode code (0-3), with "UNKNOWN" as fallback
PROGMEM_STRING_TABLE(FlashModeStrings, "QIO", "QOUT", "DIO", "DOUT", "UNKNOWN");
static_assert(FM_QIO == 0, "Flash mode enum values must match table indices");
static_assert(FM_QOUT == 1, "Flash mode enum values must match table indices");
static_assert(FM_DIO == 2, "Flash mode enum values must match table indices");
static_assert(FM_DOUT == 3, "Flash mode enum values must match table indices");
// Get reset reason string from reason code (no heap allocation) // Get reset reason string from reason code (no heap allocation)
// Returns LogString* pointing to flash (PROGMEM) on ESP8266 // Returns LogString* pointing to flash (PROGMEM) on ESP8266
static const LogString *get_reset_reason_str(uint32_t reason) { static const LogString *get_reset_reason_str(uint32_t reason) {
switch (reason) { return ResetReasonStrings::get_log_str(static_cast<uint8_t>(reason), ResetReasonStrings::LAST_INDEX);
case REASON_DEFAULT_RST:
return LOG_STR("Power On");
case REASON_WDT_RST:
return LOG_STR("Hardware Watchdog");
case REASON_EXCEPTION_RST:
return LOG_STR("Exception");
case REASON_SOFT_WDT_RST:
return LOG_STR("Software Watchdog");
case REASON_SOFT_RESTART:
return LOG_STR("Software/System restart");
case REASON_DEEP_SLEEP_AWAKE:
return LOG_STR("Deep-Sleep Wake");
case REASON_EXT_SYS_RST:
return LOG_STR("External System");
default:
return LOG_STR("Unknown");
}
} }
// Size for core version hex buffer // Size for core version hex buffer
@@ -92,23 +104,9 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE; constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data(); char *buf = buffer.data();
const LogString *flash_mode; const LogString *flash_mode = FlashModeStrings::get_log_str(
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance) static_cast<uint8_t>(ESP.getFlashChipMode()), // NOLINT(readability-static-accessed-through-instance)
case FM_QIO: FlashModeStrings::LAST_INDEX);
flash_mode = LOG_STR("QIO");
break;
case FM_QOUT:
flash_mode = LOG_STR("QOUT");
break;
case FM_DIO:
flash_mode = LOG_STR("DIO");
break;
case FM_DOUT:
flash_mode = LOG_STR("DOUT");
break;
default:
flash_mode = LOG_STR("UNKNOWN");
}
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance) uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance) uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance)
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed,

View File

@@ -1436,10 +1436,6 @@ async def to_code(config):
CORE.relative_internal_path(".espressif") CORE.relative_internal_path(".espressif")
) )
# Set the uv cache inside the data dir so "Clean All" clears it.
# Avoids persistent corrupted cache from mid-stream download failures.
os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache"))
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")

View File

@@ -90,16 +90,14 @@ void HttpRequestUpdate::update_task(void *params) {
UPDATE_RETURN; UPDATE_RETURN;
} }
size_t read_index = container->get_bytes_read(); size_t read_index = container->get_bytes_read();
size_t content_length = container->content_length;
container->end();
container.reset(); // Release ownership of the container's shared_ptr
bool valid = false; bool valid = false;
{ // Ensures the response string falls out of scope and deallocates before the task ends { // Scope to ensure JsonDocument is destroyed before deallocating buffer
std::string response((char *) data, read_index); valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
allocator.deallocate(data, container->content_length);
container->end();
container.reset(); // Release ownership of the container's shared_ptr
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() || if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
!root[ESPHOME_F("builds")].is<JsonArray>()) { !root[ESPHOME_F("builds")].is<JsonArray>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields"); ESP_LOGE(TAG, "Manifest does not contain required fields");
@@ -137,6 +135,7 @@ void HttpRequestUpdate::update_task(void *params) {
return false; return false;
}); });
} }
allocator.deallocate(data, content_length);
if (!valid) { if (!valid) {
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
@@ -157,17 +156,12 @@ void HttpRequestUpdate::update_task(void *params) {
} }
} }
{ // Ensures the current version string falls out of scope and deallocates before the task ends
std::string current_version;
#ifdef ESPHOME_PROJECT_VERSION #ifdef ESPHOME_PROJECT_VERSION
current_version = ESPHOME_PROJECT_VERSION; this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
#else #else
current_version = ESPHOME_VERSION; this_update->update_info_.current_version = ESPHOME_VERSION;
#endif #endif
this_update->update_info_.current_version = current_version;
}
bool trigger_update_available = false; bool trigger_update_available = false;
if (this_update->update_info_.latest_version.empty() || if (this_update->update_info_.latest_version.empty() ||

View File

@@ -25,8 +25,13 @@ std::string build_json(const json_build_t &f) {
} }
bool parse_json(const std::string &data, const json_parse_t &f) { bool parse_json(const std::string &data, const json_parse_t &f) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size(), f);
}
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size()); JsonDocument doc = parse_json(data, len);
if (doc.overflowed() || doc.isNull()) if (doc.overflowed() || doc.isNull())
return false; return false;
return f(doc.as<JsonObject>()); return f(doc.as<JsonObject>());

View File

@@ -50,6 +50,8 @@ std::string build_json(const json_build_t &f);
/// Parse a JSON string and run the provided json parse function if it's valid. /// Parse a JSON string and run the provided json parse function if it's valid.
bool parse_json(const std::string &data, const json_parse_t &f); bool parse_json(const std::string &data, const json_parse_t &f);
/// Parse JSON from raw bytes and run the provided json parse function if it's valid.
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f);
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error) /// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
JsonDocument parse_json(const uint8_t *data, size_t len); JsonDocument parse_json(const uint8_t *data, size_t len);

View File

@@ -193,14 +193,14 @@ def _notify_old_style(config):
# The dev and latest branches will be at *least* this version, which is what matters. # The dev and latest branches will be at *least* this version, which is what matters.
# Use GitHub releases directly to avoid PlatformIO moderation delays. # Use GitHub releases directly to avoid PlatformIO moderation delays.
ARDUINO_VERSIONS = { ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"), "dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"),
"latest": ( "latest": (
cv.Version(1, 11, 0), cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.11.0", "https://github.com/libretiny-eu/libretiny.git#v1.12.1",
), ),
"recommended": ( "recommended": (
cv.Version(1, 11, 0), cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.11.0", "https://github.com/libretiny-eu/libretiny.git#v1.12.1",
), ),
} }

View File

@@ -154,28 +154,26 @@ LN882X_BOARD_PINS = {
"A7": 21, "A7": 21,
}, },
"wb02a": { "wb02a": {
"WIRE0_SCL_0": 7, "WIRE0_SCL_0": 1,
"WIRE0_SCL_1": 5, "WIRE0_SCL_1": 2,
"WIRE0_SCL_2": 3, "WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 10, "WIRE0_SCL_3": 4,
"WIRE0_SCL_4": 2, "WIRE0_SCL_4": 5,
"WIRE0_SCL_5": 1, "WIRE0_SCL_5": 7,
"WIRE0_SCL_6": 4, "WIRE0_SCL_6": 9,
"WIRE0_SCL_7": 5, "WIRE0_SCL_7": 10,
"WIRE0_SCL_8": 9, "WIRE0_SCL_8": 24,
"WIRE0_SCL_9": 24, "WIRE0_SCL_9": 25,
"WIRE0_SCL_10": 25, "WIRE0_SDA_0": 1,
"WIRE0_SDA_0": 7, "WIRE0_SDA_1": 2,
"WIRE0_SDA_1": 5,
"WIRE0_SDA_2": 3, "WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 10, "WIRE0_SDA_3": 4,
"WIRE0_SDA_4": 2, "WIRE0_SDA_4": 5,
"WIRE0_SDA_5": 1, "WIRE0_SDA_5": 7,
"WIRE0_SDA_6": 4, "WIRE0_SDA_6": 9,
"WIRE0_SDA_7": 5, "WIRE0_SDA_7": 10,
"WIRE0_SDA_8": 9, "WIRE0_SDA_8": 24,
"WIRE0_SDA_9": 24, "WIRE0_SDA_9": 25,
"WIRE0_SDA_10": 25,
"SERIAL0_RX": 3, "SERIAL0_RX": 3,
"SERIAL0_TX": 2, "SERIAL0_TX": 2,
"SERIAL1_RX": 24, "SERIAL1_RX": 24,
@@ -221,32 +219,32 @@ LN882X_BOARD_PINS = {
"A1": 4, "A1": 4,
}, },
"wl2s": { "wl2s": {
"WIRE0_SCL_0": 7, "WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 12, "WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 3, "WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 10, "WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 2, "WIRE0_SCL_4": 5,
"WIRE0_SCL_5": 0, "WIRE0_SCL_5": 7,
"WIRE0_SCL_6": 19, "WIRE0_SCL_6": 9,
"WIRE0_SCL_7": 11, "WIRE0_SCL_7": 10,
"WIRE0_SCL_8": 9, "WIRE0_SCL_8": 11,
"WIRE0_SCL_9": 24, "WIRE0_SCL_9": 12,
"WIRE0_SCL_10": 25, "WIRE0_SCL_10": 19,
"WIRE0_SCL_11": 5, "WIRE0_SCL_11": 24,
"WIRE0_SCL_12": 1, "WIRE0_SCL_12": 25,
"WIRE0_SDA_0": 7, "WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 12, "WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 3, "WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 10, "WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 2, "WIRE0_SDA_4": 5,
"WIRE0_SDA_5": 0, "WIRE0_SDA_5": 7,
"WIRE0_SDA_6": 19, "WIRE0_SDA_6": 9,
"WIRE0_SDA_7": 11, "WIRE0_SDA_7": 10,
"WIRE0_SDA_8": 9, "WIRE0_SDA_8": 11,
"WIRE0_SDA_9": 24, "WIRE0_SDA_9": 12,
"WIRE0_SDA_10": 25, "WIRE0_SDA_10": 19,
"WIRE0_SDA_11": 5, "WIRE0_SDA_11": 24,
"WIRE0_SDA_12": 1, "WIRE0_SDA_12": 25,
"SERIAL0_RX": 3, "SERIAL0_RX": 3,
"SERIAL0_TX": 2, "SERIAL0_TX": 2,
"SERIAL1_RX": 24, "SERIAL1_RX": 24,
@@ -301,24 +299,24 @@ LN882X_BOARD_PINS = {
"A2": 1, "A2": 1,
}, },
"ln-02": { "ln-02": {
"WIRE0_SCL_0": 11, "WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 19, "WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 3, "WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 24, "WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 2, "WIRE0_SCL_4": 9,
"WIRE0_SCL_5": 25, "WIRE0_SCL_5": 11,
"WIRE0_SCL_6": 1, "WIRE0_SCL_6": 19,
"WIRE0_SCL_7": 0, "WIRE0_SCL_7": 24,
"WIRE0_SCL_8": 9, "WIRE0_SCL_8": 25,
"WIRE0_SDA_0": 11, "WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 19, "WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 3, "WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 24, "WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 2, "WIRE0_SDA_4": 9,
"WIRE0_SDA_5": 25, "WIRE0_SDA_5": 11,
"WIRE0_SDA_6": 1, "WIRE0_SDA_6": 19,
"WIRE0_SDA_7": 0, "WIRE0_SDA_7": 24,
"WIRE0_SDA_8": 9, "WIRE0_SDA_8": 25,
"SERIAL0_RX": 3, "SERIAL0_RX": 3,
"SERIAL0_TX": 2, "SERIAL0_TX": 2,
"SERIAL1_RX": 24, "SERIAL1_RX": 24,

View File

@@ -45,9 +45,28 @@ class MDNSComponent : public Component {
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_ARDUINO) // Polling interval for MDNS.update() on platforms that require it (ESP8266, RP2040).
void loop() override; //
#endif // On these platforms, MDNS.update() calls _process(true) which only manages timer-driven
// state machines (probe/announce timeouts and service query cache TTLs). Incoming mDNS
// packets are handled independently via the lwIP onRx UDP callback and are NOT affected
// by how often update() is called.
//
// The shortest internal timer is the 250ms probe interval (RFC 6762 Section 8.1).
// Announcement intervals are 1000ms and cache TTL checks are on the order of seconds
// to minutes. A 50ms polling interval provides sufficient resolution for all timers
// while completely removing mDNS from the per-iteration loop list.
//
// In steady state (after the ~8 second boot probe/announce phase completes), update()
// checks timers that are set to never expire, making every call pure overhead.
//
// Tasmota uses a 50ms main loop cycle with mDNS working correctly, confirming this
// interval is safe in production.
//
// By using set_interval() instead of overriding loop(), the component is excluded from
// the main loop list via has_overridden_loop(), eliminating all per-iteration overhead
// including virtual dispatch.
static constexpr uint32_t MDNS_UPDATE_INTERVAL_MS = 50;
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
#ifdef USE_MDNS_EXTRA_SERVICES #ifdef USE_MDNS_EXTRA_SERVICES

View File

@@ -36,9 +36,14 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
} }
} }
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp8266); } void MDNSComponent::setup() {
this->setup_buffers_and_register_(register_esp8266);
void MDNSComponent::loop() { MDNS.update(); } // Schedule MDNS.update() via set_interval() instead of overriding loop().
// This removes the component from the per-iteration loop list entirely,
// eliminating virtual dispatch overhead on every main loop cycle.
// See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis.
this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
}
void MDNSComponent::on_shutdown() { void MDNSComponent::on_shutdown() {
MDNS.close(); MDNS.close();

View File

@@ -35,9 +35,14 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
} }
} }
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_rp2040); } void MDNSComponent::setup() {
this->setup_buffers_and_register_(register_rp2040);
void MDNSComponent::loop() { MDNS.update(); } // Schedule MDNS.update() via set_interval() instead of overriding loop().
// This removes the component from the per-iteration loop list entirely,
// eliminating virtual dispatch overhead on every main loop cycle.
// See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis.
this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
}
void MDNSComponent::on_shutdown() { void MDNSComponent::on_shutdown() {
MDNS.close(); MDNS.close();

View File

@@ -170,10 +170,8 @@ void MQTTClientComponent::send_device_info_() {
void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
(void) tag; (void) tag;
if (level <= this->log_level_ && this->is_connected()) { if (level <= this->log_level_ && this->is_connected()) {
this->publish({.topic = this->log_message_.topic, this->publish(this->log_message_.topic.c_str(), message, message_len, this->log_message_.qos,
.payload = std::string(message, message_len), this->log_message_.retain);
.qos = this->log_message_.qos,
.retain = this->log_message_.retain});
} }
} }
#endif #endif

View File

@@ -300,9 +300,11 @@ const EntityBase *MQTTClimateComponent::get_entity() const { return this->device
bool MQTTClimateComponent::publish_state_() { bool MQTTClimateComponent::publish_state_() {
auto traits = this->device_->get_traits(); auto traits = this->device_->get_traits();
// Reusable stack buffer for topic construction (avoids heap allocation per publish)
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
// mode // mode
bool success = true; bool success = true;
if (!this->publish(this->get_mode_state_topic(), climate_mode_to_mqtt_str(this->device_->mode))) if (!this->publish(this->get_mode_state_topic_to(topic_buf), climate_mode_to_mqtt_str(this->device_->mode)))
success = false; success = false;
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
@@ -311,68 +313,70 @@ bool MQTTClimateComponent::publish_state_() {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) && if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) &&
!std::isnan(this->device_->current_temperature)) { !std::isnan(this->device_->current_temperature)) {
len = value_accuracy_to_buf(payload, this->device_->current_temperature, current_accuracy); len = value_accuracy_to_buf(payload, this->device_->current_temperature, current_accuracy);
if (!this->publish(this->get_current_temperature_state_topic(), payload, len)) if (!this->publish(this->get_current_temperature_state_topic_to(topic_buf), payload, len))
success = false; success = false;
} }
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
len = value_accuracy_to_buf(payload, this->device_->target_temperature_low, target_accuracy); len = value_accuracy_to_buf(payload, this->device_->target_temperature_low, target_accuracy);
if (!this->publish(this->get_target_temperature_low_state_topic(), payload, len)) if (!this->publish(this->get_target_temperature_low_state_topic_to(topic_buf), payload, len))
success = false; success = false;
len = value_accuracy_to_buf(payload, this->device_->target_temperature_high, target_accuracy); len = value_accuracy_to_buf(payload, this->device_->target_temperature_high, target_accuracy);
if (!this->publish(this->get_target_temperature_high_state_topic(), payload, len)) if (!this->publish(this->get_target_temperature_high_state_topic_to(topic_buf), payload, len))
success = false; success = false;
} else { } else {
len = value_accuracy_to_buf(payload, this->device_->target_temperature, target_accuracy); len = value_accuracy_to_buf(payload, this->device_->target_temperature, target_accuracy);
if (!this->publish(this->get_target_temperature_state_topic(), payload, len)) if (!this->publish(this->get_target_temperature_state_topic_to(topic_buf), payload, len))
success = false; success = false;
} }
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) && if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) &&
!std::isnan(this->device_->current_humidity)) { !std::isnan(this->device_->current_humidity)) {
len = value_accuracy_to_buf(payload, this->device_->current_humidity, 0); len = value_accuracy_to_buf(payload, this->device_->current_humidity, 0);
if (!this->publish(this->get_current_humidity_state_topic(), payload, len)) if (!this->publish(this->get_current_humidity_state_topic_to(topic_buf), payload, len))
success = false; success = false;
} }
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) && if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) &&
!std::isnan(this->device_->target_humidity)) { !std::isnan(this->device_->target_humidity)) {
len = value_accuracy_to_buf(payload, this->device_->target_humidity, 0); len = value_accuracy_to_buf(payload, this->device_->target_humidity, 0);
if (!this->publish(this->get_target_humidity_state_topic(), payload, len)) if (!this->publish(this->get_target_humidity_state_topic_to(topic_buf), payload, len))
success = false; success = false;
} }
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
if (this->device_->has_custom_preset()) { if (this->device_->has_custom_preset()) {
if (!this->publish(this->get_preset_state_topic(), this->device_->get_custom_preset())) if (!this->publish(this->get_preset_state_topic_to(topic_buf), this->device_->get_custom_preset().c_str()))
success = false; success = false;
} else if (this->device_->preset.has_value()) { } else if (this->device_->preset.has_value()) {
if (!this->publish(this->get_preset_state_topic(), climate_preset_to_mqtt_str(this->device_->preset.value()))) if (!this->publish(this->get_preset_state_topic_to(topic_buf),
climate_preset_to_mqtt_str(this->device_->preset.value())))
success = false; success = false;
} else if (!this->publish(this->get_preset_state_topic(), "")) { } else if (!this->publish(this->get_preset_state_topic_to(topic_buf), "")) {
success = false; success = false;
} }
} }
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
if (!this->publish(this->get_action_state_topic(), climate_action_to_mqtt_str(this->device_->action))) if (!this->publish(this->get_action_state_topic_to(topic_buf), climate_action_to_mqtt_str(this->device_->action)))
success = false; success = false;
} }
if (traits.get_supports_fan_modes()) { if (traits.get_supports_fan_modes()) {
if (this->device_->has_custom_fan_mode()) { if (this->device_->has_custom_fan_mode()) {
if (!this->publish(this->get_fan_mode_state_topic(), this->device_->get_custom_fan_mode())) if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), this->device_->get_custom_fan_mode().c_str()))
success = false; success = false;
} else if (this->device_->fan_mode.has_value()) { } else if (this->device_->fan_mode.has_value()) {
if (!this->publish(this->get_fan_mode_state_topic(), if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf),
climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value()))) climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value())))
success = false; success = false;
} else if (!this->publish(this->get_fan_mode_state_topic(), "")) { } else if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), "")) {
success = false; success = false;
} }
} }
if (traits.get_supports_swing_modes()) { if (traits.get_supports_swing_modes()) {
if (!this->publish(this->get_swing_mode_state_topic(), climate_swing_mode_to_mqtt_str(this->device_->swing_mode))) if (!this->publish(this->get_swing_mode_state_topic_to(topic_buf),
climate_swing_mode_to_mqtt_str(this->device_->swing_mode)))
success = false; success = false;
} }

View File

@@ -59,6 +59,11 @@ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, b
\ \
public: \ public: \
void set_custom_##name##_##type##_topic(const std::string &topic) { this->custom_##name##_##type##_topic_ = topic; } \ void set_custom_##name##_##type##_topic(const std::string &topic) { this->custom_##name##_##type##_topic_ = topic; } \
StringRef get_##name##_##type##_topic_to(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const { \
if (!this->custom_##name##_##type##_topic_.empty()) \
return StringRef(this->custom_##name##_##type##_topic_.data(), this->custom_##name##_##type##_topic_.size()); \
return this->get_default_topic_for_to_(buf, #name "/" #type, sizeof(#name "/" #type) - 1); \
} \
std::string get_##name##_##type##_topic() const { \ std::string get_##name##_##type##_topic() const { \
if (this->custom_##name##_##type##_topic_.empty()) \ if (this->custom_##name##_##type##_topic_.empty()) \
return this->get_default_topic_for_(#name "/" #type); \ return this->get_default_topic_for_(#name "/" #type); \

View File

@@ -112,19 +112,19 @@ bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
bool MQTTCoverComponent::publish_state() { bool MQTTCoverComponent::publish_state() {
auto traits = this->cover_->get_traits(); auto traits = this->cover_->get_traits();
bool success = true; bool success = true;
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (traits.get_supports_position()) { if (traits.get_supports_position()) {
char pos[VALUE_ACCURACY_MAX_LEN]; char pos[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0); size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0);
if (!this->publish(this->get_position_state_topic(), pos, len)) if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len))
success = false; success = false;
} }
if (traits.get_supports_tilt()) { if (traits.get_supports_tilt()) {
char pos[VALUE_ACCURACY_MAX_LEN]; char pos[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->tilt * 100), 0); size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->tilt * 100), 0);
if (!this->publish(this->get_tilt_state_topic(), pos, len)) if (!this->publish(this->get_tilt_state_topic_to(topic_buf), pos, len))
success = false; success = false;
} }
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf), if (!this->publish(this->get_state_topic_to_(topic_buf),
cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position, cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
traits.get_supports_position()))) traits.get_supports_position())))

View File

@@ -173,19 +173,20 @@ bool MQTTFanComponent::publish_state() {
this->publish(this->get_state_topic_to_(topic_buf), state_s); this->publish(this->get_state_topic_to_(topic_buf), state_s);
bool failed = false; bool failed = false;
if (this->state_->get_traits().supports_direction()) { if (this->state_->get_traits().supports_direction()) {
bool success = this->publish(this->get_direction_state_topic(), fan_direction_to_mqtt_str(this->state_->direction)); bool success = this->publish(this->get_direction_state_topic_to(topic_buf),
fan_direction_to_mqtt_str(this->state_->direction));
failed = failed || !success; failed = failed || !success;
} }
if (this->state_->get_traits().supports_oscillation()) { if (this->state_->get_traits().supports_oscillation()) {
bool success = bool success = this->publish(this->get_oscillation_state_topic_to(topic_buf),
this->publish(this->get_oscillation_state_topic(), fan_oscillation_to_mqtt_str(this->state_->oscillating)); fan_oscillation_to_mqtt_str(this->state_->oscillating));
failed = failed || !success; failed = failed || !success;
} }
auto traits = this->state_->get_traits(); auto traits = this->state_->get_traits();
if (traits.supports_speed()) { if (traits.supports_speed()) {
char buf[12]; char buf[12];
size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed); size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed);
bool success = this->publish(this->get_speed_level_state_topic(), buf, len); bool success = this->publish(this->get_speed_level_state_topic_to(topic_buf), buf, len);
failed = failed || !success; failed = failed || !success;
} }
return !failed; return !failed;

View File

@@ -87,13 +87,13 @@ bool MQTTValveComponent::send_initial_state() { return this->publish_state(); }
bool MQTTValveComponent::publish_state() { bool MQTTValveComponent::publish_state() {
auto traits = this->valve_->get_traits(); auto traits = this->valve_->get_traits();
bool success = true; bool success = true;
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (traits.get_supports_position()) { if (traits.get_supports_position()) {
char pos[VALUE_ACCURACY_MAX_LEN]; char pos[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(pos, roundf(this->valve_->position * 100), 0); size_t len = value_accuracy_to_buf(pos, roundf(this->valve_->position * 100), 0);
if (!this->publish(this->get_position_state_topic(), pos, len)) if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len))
success = false; success = false;
} }
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf), if (!this->publish(this->get_state_topic_to_(topic_buf),
valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position, valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position,
traits.get_supports_position()))) traits.get_supports_position())))

View File

@@ -71,6 +71,10 @@ RTL87XX_BOARDS = {
"name": "WR3L Wi-Fi Module", "name": "WR3L Wi-Fi Module",
"family": FAMILY_RTL8710B, "family": FAMILY_RTL8710B,
}, },
"wbru": {
"name": "WBRU Wi-Fi Module",
"family": FAMILY_RTL8720C,
},
"wr2le": { "wr2le": {
"name": "WR2LE Wi-Fi Module", "name": "WR2LE Wi-Fi Module",
"family": FAMILY_RTL8710B, "family": FAMILY_RTL8710B,
@@ -83,6 +87,14 @@ RTL87XX_BOARDS = {
"name": "T103_V1.0", "name": "T103_V1.0",
"family": FAMILY_RTL8710B, "family": FAMILY_RTL8710B,
}, },
"cr3l": {
"name": "CR3L Wi-Fi Module",
"family": FAMILY_RTL8720C,
},
"generic-rtl8720cm-4mb-1712k": {
"name": "Generic - RTL8720CM (4M/1712k)",
"family": FAMILY_RTL8720C,
},
"generic-rtl8720cf-2mb-896k": { "generic-rtl8720cf-2mb-896k": {
"name": "Generic - RTL8720CF (2M/896k)", "name": "Generic - RTL8720CF (2M/896k)",
"family": FAMILY_RTL8720C, "family": FAMILY_RTL8720C,
@@ -103,6 +115,10 @@ RTL87XX_BOARDS = {
"name": "WR2L Wi-Fi Module", "name": "WR2L Wi-Fi Module",
"family": FAMILY_RTL8710B, "family": FAMILY_RTL8710B,
}, },
"wbr1": {
"name": "WBR1 Wi-Fi Module",
"family": FAMILY_RTL8720C,
},
"wr1": { "wr1": {
"name": "WR1 Wi-Fi Module", "name": "WR1 Wi-Fi Module",
"family": FAMILY_RTL8710B, "family": FAMILY_RTL8710B,
@@ -119,10 +135,10 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22, "SPI1_MISO": 22,
"SPI1_MOSI": 23, "SPI1_MOSI": 23,
"SPI1_SCK": 18, "SPI1_SCK": 18,
"WIRE0_SCL_0": 29, "WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 22, "WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 30, "WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 19, "WIRE0_SDA_1": 30,
"WIRE1_SCL": 18, "WIRE1_SCL": 18,
"WIRE1_SDA": 23, "WIRE1_SDA": 23,
"SERIAL0_CTS": 19, "SERIAL0_CTS": 19,
@@ -230,10 +246,10 @@ RTL87XX_BOARD_PINS = {
"A1": 41, "A1": 41,
}, },
"wbr3": { "wbr3": {
"WIRE0_SCL_0": 11, "WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 2, "WIRE0_SCL_1": 11,
"WIRE0_SCL_2": 19, "WIRE0_SCL_2": 15,
"WIRE0_SCL_3": 15, "WIRE0_SCL_3": 19,
"WIRE0_SDA_0": 3, "WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12, "WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16, "WIRE0_SDA_2": 16,
@@ -242,10 +258,10 @@ RTL87XX_BOARD_PINS = {
"SERIAL0_TX_0": 11, "SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14, "SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4, "SERIAL1_CTS": 4,
"SERIAL1_RX_0": 2, "SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 0, "SERIAL1_RX_1": 2,
"SERIAL1_TX_0": 3, "SERIAL1_TX_0": 1,
"SERIAL1_TX_1": 1, "SERIAL1_TX_1": 3,
"SERIAL2_CTS": 19, "SERIAL2_CTS": 19,
"SERIAL2_RX": 15, "SERIAL2_RX": 15,
"SERIAL2_TX": 16, "SERIAL2_TX": 16,
@@ -296,6 +312,12 @@ RTL87XX_BOARD_PINS = {
}, },
"generic-rtl8710bn-2mb-468k": { "generic-rtl8710bn-2mb-468k": {
"SPI0_CS": 19, "SPI0_CS": 19,
"SPI0_FCS": 6,
"SPI0_FD0": 9,
"SPI0_FD1": 7,
"SPI0_FD2": 8,
"SPI0_FD3": 11,
"SPI0_FSCK": 10,
"SPI0_MISO": 22, "SPI0_MISO": 22,
"SPI0_MOSI": 23, "SPI0_MOSI": 23,
"SPI0_SCK": 18, "SPI0_SCK": 18,
@@ -396,10 +418,10 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22, "SPI1_MISO": 22,
"SPI1_MOSI": 23, "SPI1_MOSI": 23,
"SPI1_SCK": 18, "SPI1_SCK": 18,
"WIRE0_SCL_0": 29, "WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 22, "WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 30, "WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 19, "WIRE0_SDA_1": 30,
"WIRE1_SCL": 18, "WIRE1_SCL": 18,
"WIRE1_SDA": 23, "WIRE1_SDA": 23,
"SERIAL0_CTS": 19, "SERIAL0_CTS": 19,
@@ -463,10 +485,10 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22, "SPI1_MISO": 22,
"SPI1_MOSI": 23, "SPI1_MOSI": 23,
"SPI1_SCK": 18, "SPI1_SCK": 18,
"WIRE0_SCL_0": 29, "WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 22, "WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 30, "WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 19, "WIRE0_SDA_1": 30,
"WIRE1_SCL": 18, "WIRE1_SCL": 18,
"WIRE1_SDA": 23, "WIRE1_SDA": 23,
"SERIAL0_CTS": 19, "SERIAL0_CTS": 19,
@@ -714,6 +736,12 @@ RTL87XX_BOARD_PINS = {
}, },
"generic-rtl8710bn-2mb-788k": { "generic-rtl8710bn-2mb-788k": {
"SPI0_CS": 19, "SPI0_CS": 19,
"SPI0_FCS": 6,
"SPI0_FD0": 9,
"SPI0_FD1": 7,
"SPI0_FD2": 8,
"SPI0_FD3": 11,
"SPI0_FSCK": 10,
"SPI0_MISO": 22, "SPI0_MISO": 22,
"SPI0_MOSI": 23, "SPI0_MOSI": 23,
"SPI0_SCK": 18, "SPI0_SCK": 18,
@@ -807,6 +835,12 @@ RTL87XX_BOARD_PINS = {
}, },
"generic-rtl8710bx-4mb-980k": { "generic-rtl8710bx-4mb-980k": {
"SPI0_CS": 19, "SPI0_CS": 19,
"SPI0_FCS": 6,
"SPI0_FD0": 9,
"SPI0_FD1": 7,
"SPI0_FD2": 8,
"SPI0_FD3": 11,
"SPI0_FSCK": 10,
"SPI0_MISO": 22, "SPI0_MISO": 22,
"SPI0_MOSI": 23, "SPI0_MOSI": 23,
"SPI0_SCK": 18, "SPI0_SCK": 18,
@@ -957,8 +991,8 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22, "SPI1_MISO": 22,
"SPI1_MOSI": 23, "SPI1_MOSI": 23,
"SPI1_SCK": 18, "SPI1_SCK": 18,
"WIRE0_SCL_0": 29, "WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 22, "WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 19, "WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 30, "WIRE0_SDA_1": 30,
"WIRE1_SCL": 18, "WIRE1_SCL": 18,
@@ -1088,6 +1122,99 @@ RTL87XX_BOARD_PINS = {
"A0": 19, "A0": 19,
"A1": 41, "A1": 41,
}, },
"wbru": {
"SPI0_CS_0": 2,
"SPI0_CS_1": 7,
"SPI0_CS_2": 15,
"SPI0_MISO_0": 10,
"SPI0_MISO_1": 20,
"SPI0_MOSI_0": 4,
"SPI0_MOSI_1": 9,
"SPI0_MOSI_2": 19,
"SPI0_SCK_0": 3,
"SPI0_SCK_1": 8,
"SPI0_SCK_2": 16,
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 11,
"WIRE0_SCL_2": 15,
"WIRE0_SCL_3": 19,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16,
"WIRE0_SDA_3": 20,
"SERIAL0_CTS": 10,
"SERIAL0_RTS": 9,
"SERIAL0_RX_0": 12,
"SERIAL0_RX_1": 13,
"SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 2,
"SERIAL1_TX": 3,
"SERIAL2_CTS": 19,
"SERIAL2_RTS": 20,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CS0": 7,
"CTS0": 10,
"CTS1": 4,
"CTS2": 19,
"MOSI0": 19,
"PA00": 0,
"PA0": 0,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA07": 7,
"PA7": 7,
"PA08": 8,
"PA8": 8,
"PA09": 9,
"PA9": 9,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PA19": 19,
"PA20": 20,
"PWM0": 0,
"PWM1": 12,
"PWM5": 17,
"PWM6": 18,
"RTS0": 9,
"RTS2": 20,
"RX2": 15,
"SCK0": 16,
"TX1": 3,
"TX2": 16,
"D0": 8,
"D1": 9,
"D2": 2,
"D3": 3,
"D4": 4,
"D5": 15,
"D6": 16,
"D7": 11,
"D8": 12,
"D9": 17,
"D10": 18,
"D11": 19,
"D12": 14,
"D13": 13,
"D14": 20,
"D15": 0,
"D16": 10,
"D17": 7,
},
"wr2le": { "wr2le": {
"MISO0": 22, "MISO0": 22,
"MISO1": 22, "MISO1": 22,
@@ -1116,21 +1243,21 @@ RTL87XX_BOARD_PINS = {
"SPI0_MISO": 20, "SPI0_MISO": 20,
"SPI0_MOSI_0": 4, "SPI0_MOSI_0": 4,
"SPI0_MOSI_1": 19, "SPI0_MOSI_1": 19,
"SPI0_SCK_0": 16, "SPI0_SCK_0": 3,
"SPI0_SCK_1": 3, "SPI0_SCK_1": 16,
"WIRE0_SCL_0": 2, "WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 15, "WIRE0_SCL_1": 15,
"WIRE0_SCL_2": 19, "WIRE0_SCL_2": 19,
"WIRE0_SDA_0": 20, "WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 16, "WIRE0_SDA_1": 16,
"WIRE0_SDA_2": 3, "WIRE0_SDA_2": 20,
"SERIAL0_RX": 13, "SERIAL0_RX": 13,
"SERIAL0_TX": 14, "SERIAL0_TX": 14,
"SERIAL1_CTS": 4, "SERIAL1_CTS": 4,
"SERIAL1_RX_0": 2, "SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 0, "SERIAL1_RX_1": 2,
"SERIAL1_TX_0": 3, "SERIAL1_TX_0": 1,
"SERIAL1_TX_1": 1, "SERIAL1_TX_1": 3,
"SERIAL2_CTS": 19, "SERIAL2_CTS": 19,
"SERIAL2_RTS": 20, "SERIAL2_RTS": 20,
"SERIAL2_RX": 15, "SERIAL2_RX": 15,
@@ -1251,6 +1378,168 @@ RTL87XX_BOARD_PINS = {
"A0": 19, "A0": 19,
"A1": 41, "A1": 41,
}, },
"cr3l": {
"SPI0_CS_0": 2,
"SPI0_CS_1": 15,
"SPI0_MISO": 20,
"SPI0_MOSI_0": 4,
"SPI0_MOSI_1": 19,
"SPI0_SCK_0": 3,
"SPI0_SCK_1": 16,
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 15,
"WIRE0_SCL_2": 19,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 16,
"WIRE0_SDA_2": 20,
"SERIAL0_RX": 13,
"SERIAL0_TX": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX": 2,
"SERIAL1_TX": 3,
"SERIAL2_CTS": 19,
"SERIAL2_RTS": 20,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CTS1": 4,
"CTS2": 19,
"MISO0": 20,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PA19": 19,
"PA20": 20,
"PWM0": 20,
"PWM5": 17,
"PWM6": 18,
"RTS2": 20,
"RX0": 13,
"RX1": 2,
"RX2": 15,
"SCL0": 19,
"SDA0": 16,
"TX0": 14,
"TX1": 3,
"TX2": 16,
"D0": 20,
"D1": 2,
"D2": 3,
"D3": 4,
"D4": 15,
"D5": 16,
"D6": 17,
"D7": 18,
"D8": 19,
"D9": 13,
"D10": 14,
},
"generic-rtl8720cm-4mb-1712k": {
"SPI0_CS_0": 2,
"SPI0_CS_1": 7,
"SPI0_CS_2": 15,
"SPI0_MISO_0": 10,
"SPI0_MISO_1": 20,
"SPI0_MOSI_0": 4,
"SPI0_MOSI_1": 9,
"SPI0_MOSI_2": 19,
"SPI0_SCK_0": 3,
"SPI0_SCK_1": 8,
"SPI0_SCK_2": 16,
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 11,
"WIRE0_SCL_2": 15,
"WIRE0_SCL_3": 19,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16,
"WIRE0_SDA_3": 20,
"SERIAL0_CTS": 10,
"SERIAL0_RTS": 9,
"SERIAL0_RX_0": 12,
"SERIAL0_RX_1": 13,
"SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 2,
"SERIAL1_TX_0": 1,
"SERIAL1_TX_1": 3,
"SERIAL2_CTS": 19,
"SERIAL2_RTS": 20,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CS0": 15,
"CTS0": 10,
"CTS1": 4,
"CTS2": 19,
"MOSI0": 19,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA07": 7,
"PA7": 7,
"PA08": 8,
"PA8": 8,
"PA09": 9,
"PA9": 9,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PA19": 19,
"PA20": 20,
"PA23": 23,
"PWM0": 20,
"PWM5": 17,
"PWM6": 18,
"PWM7": 23,
"RTS0": 9,
"RTS2": 20,
"RX2": 15,
"SCK0": 16,
"TX2": 16,
"D0": 0,
"D1": 1,
"D2": 2,
"D3": 3,
"D4": 4,
"D5": 7,
"D6": 8,
"D7": 9,
"D8": 10,
"D9": 11,
"D10": 12,
"D11": 13,
"D12": 14,
"D13": 15,
"D14": 16,
"D15": 17,
"D16": 18,
"D17": 19,
"D18": 20,
"D19": 23,
},
"generic-rtl8720cf-2mb-896k": { "generic-rtl8720cf-2mb-896k": {
"SPI0_CS_0": 2, "SPI0_CS_0": 2,
"SPI0_CS_1": 7, "SPI0_CS_1": 7,
@@ -1456,8 +1745,8 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22, "SPI1_MISO": 22,
"SPI1_MOSI": 23, "SPI1_MOSI": 23,
"SPI1_SCK": 18, "SPI1_SCK": 18,
"WIRE0_SCL_0": 29, "WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 22, "WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 19, "WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 30, "WIRE0_SDA_1": 30,
"WIRE1_SCL": 18, "WIRE1_SCL": 18,
@@ -1585,6 +1874,65 @@ RTL87XX_BOARD_PINS = {
"D4": 12, "D4": 12,
"A0": 19, "A0": 19,
}, },
"wbr1": {
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 11,
"WIRE0_SCL_2": 15,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16,
"SERIAL0_RX_0": 12,
"SERIAL0_RX_1": 13,
"SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 2,
"SERIAL1_TX_0": 1,
"SERIAL1_TX_1": 3,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CTS1": 4,
"MOSI0": 4,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA11": 11,
"PA12": 12,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PWM5": 17,
"PWM6": 18,
"PWM7": 13,
"RX2": 15,
"SCL0": 15,
"SDA0": 12,
"TX2": 16,
"D0": 14,
"D1": 13,
"D2": 2,
"D3": 3,
"D4": 16,
"D5": 4,
"D6": 11,
"D7": 15,
"D8": 12,
"D9": 17,
"D10": 18,
"D11": 0,
"D12": 1,
},
"wr1": { "wr1": {
"SPI0_CS": 19, "SPI0_CS": 19,
"SPI0_MISO": 22, "SPI0_MISO": 22,
@@ -1594,10 +1942,10 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22, "SPI1_MISO": 22,
"SPI1_MOSI": 23, "SPI1_MOSI": 23,
"SPI1_SCK": 18, "SPI1_SCK": 18,
"WIRE0_SCL_0": 29, "WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 22, "WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 30, "WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 19, "WIRE0_SDA_1": 30,
"WIRE1_SCL": 18, "WIRE1_SCL": 18,
"WIRE1_SDA": 23, "WIRE1_SDA": 23,
"SERIAL0_CTS": 19, "SERIAL0_CTS": 19,

View File

@@ -2,6 +2,7 @@
#include <cmath> #include <cmath>
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome { namespace esphome {
namespace rtttl { namespace rtttl {
@@ -375,22 +376,13 @@ void Rtttl::loop() {
} }
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
// RTTTL state strings indexed by State enum (0-4): STOPPED, INIT, STARTING, RUNNING, STOPPING, plus UNKNOWN fallback
PROGMEM_STRING_TABLE(RtttlStateStrings, "STATE_STOPPED", "STATE_INIT", "STATE_STARTING", "STATE_RUNNING",
"STATE_STOPPING", "UNKNOWN");
static const LogString *state_to_string(State state) { static const LogString *state_to_string(State state) {
switch (state) { return RtttlStateStrings::get_log_str(static_cast<uint8_t>(state), RtttlStateStrings::LAST_INDEX);
case STATE_STOPPED: }
return LOG_STR("STATE_STOPPED");
case STATE_STARTING:
return LOG_STR("STATE_STARTING");
case STATE_RUNNING:
return LOG_STR("STATE_RUNNING");
case STATE_STOPPING:
return LOG_STR("STATE_STOPPING");
case STATE_INIT:
return LOG_STR("STATE_INIT");
default:
return LOG_STR("UNKNOWN");
}
};
#endif #endif
void Rtttl::set_state_(State state) { void Rtttl::set_state_(State state) {

View File

@@ -4,6 +4,7 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cinttypes> #include <cinttypes>
#include <utility> #include <utility>
@@ -1544,42 +1545,19 @@ void Sprinkler::log_multiplier_zero_warning_(const LogString *method_name) {
ESP_LOGW(TAG, "%s called but multiplier is set to zero; no action taken", LOG_STR_ARG(method_name)); ESP_LOGW(TAG, "%s called but multiplier is set to zero; no action taken", LOG_STR_ARG(method_name));
} }
// Request origin strings indexed by SprinklerValveRunRequestOrigin enum (0-2): USER, CYCLE, QUEUE
PROGMEM_STRING_TABLE(SprinklerRequestOriginStrings, "USER", "CYCLE", "QUEUE", "UNKNOWN");
const LogString *Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) { const LogString *Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) {
switch (origin) { return SprinklerRequestOriginStrings::get_log_str(static_cast<uint8_t>(origin),
case USER: SprinklerRequestOriginStrings::LAST_INDEX);
return LOG_STR("USER");
case CYCLE:
return LOG_STR("CYCLE");
case QUEUE:
return LOG_STR("QUEUE");
default:
return LOG_STR("UNKNOWN");
}
} }
// Sprinkler state strings indexed by SprinklerState enum (0-4): IDLE, STARTING, ACTIVE, STOPPING, BYPASS
PROGMEM_STRING_TABLE(SprinklerStateStrings, "IDLE", "STARTING", "ACTIVE", "STOPPING", "BYPASS", "UNKNOWN");
const LogString *Sprinkler::state_as_str_(SprinklerState state) { const LogString *Sprinkler::state_as_str_(SprinklerState state) {
switch (state) { return SprinklerStateStrings::get_log_str(static_cast<uint8_t>(state), SprinklerStateStrings::LAST_INDEX);
case IDLE:
return LOG_STR("IDLE");
case STARTING:
return LOG_STR("STARTING");
case ACTIVE:
return LOG_STR("ACTIVE");
case STOPPING:
return LOG_STR("STOPPING");
case BYPASS:
return LOG_STR("BYPASS");
default:
return LOG_STR("UNKNOWN");
}
} }
void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {

View File

@@ -1,6 +1,7 @@
#include "ssd1306_base.h" #include "ssd1306_base.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome { namespace esphome {
namespace ssd1306_base { namespace ssd1306_base {
@@ -40,6 +41,55 @@ static const uint8_t SSD1305_COMMAND_SET_AREA_COLOR = 0xD8;
static const uint8_t SH1107_COMMAND_SET_START_LINE = 0xDC; static const uint8_t SH1107_COMMAND_SET_START_LINE = 0xDC;
static const uint8_t SH1107_COMMAND_CHARGE_PUMP = 0xAD; static const uint8_t SH1107_COMMAND_CHARGE_PUMP = 0xAD;
// Verify first enum value and table sizes match SSD1306_MODEL_COUNT
static_assert(SSD1306_MODEL_128_32 == 0, "SSD1306Model enum must start at 0");
// PROGMEM lookup table indexed by SSD1306Model enum (width, height per model)
struct ModelDimensions {
uint8_t width;
uint8_t height;
};
static const ModelDimensions MODEL_DIMS[] PROGMEM = {
{128, 32}, // SSD1306_MODEL_128_32
{128, 64}, // SSD1306_MODEL_128_64
{96, 16}, // SSD1306_MODEL_96_16
{64, 48}, // SSD1306_MODEL_64_48
{64, 32}, // SSD1306_MODEL_64_32
{72, 40}, // SSD1306_MODEL_72_40
{128, 32}, // SH1106_MODEL_128_32
{128, 64}, // SH1106_MODEL_128_64
{96, 16}, // SH1106_MODEL_96_16
{64, 48}, // SH1106_MODEL_64_48
{64, 128}, // SH1107_MODEL_128_64 (note: width is 64, height is 128)
{128, 128}, // SH1107_MODEL_128_128
{128, 32}, // SSD1305_MODEL_128_32
{128, 64}, // SSD1305_MODEL_128_64
};
// clang-format off
PROGMEM_STRING_TABLE(ModelStrings,
"SSD1306 128x32", // SSD1306_MODEL_128_32
"SSD1306 128x64", // SSD1306_MODEL_128_64
"SSD1306 96x16", // SSD1306_MODEL_96_16
"SSD1306 64x48", // SSD1306_MODEL_64_48
"SSD1306 64x32", // SSD1306_MODEL_64_32
"SSD1306 72x40", // SSD1306_MODEL_72_40
"SH1106 128x32", // SH1106_MODEL_128_32
"SH1106 128x64", // SH1106_MODEL_128_64
"SH1106 96x16", // SH1106_MODEL_96_16
"SH1106 64x48", // SH1106_MODEL_64_48
"SH1107 128x64", // SH1107_MODEL_128_64
"SH1107 128x128", // SH1107_MODEL_128_128
"SSD1305 128x32", // SSD1305_MODEL_128_32
"SSD1305 128x64", // SSD1305_MODEL_128_64
"Unknown" // fallback
);
// clang-format on
static_assert(sizeof(MODEL_DIMS) / sizeof(MODEL_DIMS[0]) == SSD1306_MODEL_COUNT,
"MODEL_DIMS must have one entry per SSD1306Model");
static_assert(ModelStrings::COUNT == SSD1306_MODEL_COUNT + 1,
"ModelStrings must have one entry per SSD1306Model plus fallback");
void SSD1306::setup() { void SSD1306::setup() {
this->init_internal_(this->get_buffer_length_()); this->init_internal_(this->get_buffer_length_());
@@ -146,6 +196,7 @@ void SSD1306::setup() {
break; break;
case SH1107_MODEL_128_64: case SH1107_MODEL_128_64:
case SH1107_MODEL_128_128: case SH1107_MODEL_128_128:
case SSD1306_MODEL_COUNT:
// Not used, but prevents build warning // Not used, but prevents build warning
break; break;
} }
@@ -274,54 +325,14 @@ void SSD1306::turn_off() {
this->is_on_ = false; this->is_on_ = false;
} }
int SSD1306::get_height_internal() { int SSD1306::get_height_internal() {
switch (this->model_) { if (this->model_ >= SSD1306_MODEL_COUNT)
case SH1107_MODEL_128_64: return 0;
case SH1107_MODEL_128_128: return progmem_read_byte(&MODEL_DIMS[this->model_].height);
return 128;
case SSD1306_MODEL_128_32:
case SSD1306_MODEL_64_32:
case SH1106_MODEL_128_32:
case SSD1305_MODEL_128_32:
return 32;
case SSD1306_MODEL_128_64:
case SH1106_MODEL_128_64:
case SSD1305_MODEL_128_64:
return 64;
case SSD1306_MODEL_96_16:
case SH1106_MODEL_96_16:
return 16;
case SSD1306_MODEL_64_48:
case SH1106_MODEL_64_48:
return 48;
case SSD1306_MODEL_72_40:
return 40;
default:
return 0;
}
} }
int SSD1306::get_width_internal() { int SSD1306::get_width_internal() {
switch (this->model_) { if (this->model_ >= SSD1306_MODEL_COUNT)
case SSD1306_MODEL_128_32: return 0;
case SH1106_MODEL_128_32: return progmem_read_byte(&MODEL_DIMS[this->model_].width);
case SSD1306_MODEL_128_64:
case SH1106_MODEL_128_64:
case SSD1305_MODEL_128_32:
case SSD1305_MODEL_128_64:
case SH1107_MODEL_128_128:
return 128;
case SSD1306_MODEL_96_16:
case SH1106_MODEL_96_16:
return 96;
case SSD1306_MODEL_64_48:
case SSD1306_MODEL_64_32:
case SH1106_MODEL_64_48:
case SH1107_MODEL_128_64:
return 64;
case SSD1306_MODEL_72_40:
return 72;
default:
return 0;
}
} }
size_t SSD1306::get_buffer_length_() { size_t SSD1306::get_buffer_length_() {
return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u;
@@ -361,37 +372,8 @@ void SSD1306::init_reset_() {
this->reset_pin_->digital_write(true); this->reset_pin_->digital_write(true);
} }
} }
const char *SSD1306::model_str_() { const LogString *SSD1306::model_str_() {
switch (this->model_) { return ModelStrings::get_log_str(static_cast<uint8_t>(this->model_), ModelStrings::LAST_INDEX);
case SSD1306_MODEL_128_32:
return "SSD1306 128x32";
case SSD1306_MODEL_128_64:
return "SSD1306 128x64";
case SSD1306_MODEL_64_32:
return "SSD1306 64x32";
case SSD1306_MODEL_96_16:
return "SSD1306 96x16";
case SSD1306_MODEL_64_48:
return "SSD1306 64x48";
case SSD1306_MODEL_72_40:
return "SSD1306 72x40";
case SH1106_MODEL_128_32:
return "SH1106 128x32";
case SH1106_MODEL_128_64:
return "SH1106 128x64";
case SH1106_MODEL_96_16:
return "SH1106 96x16";
case SH1106_MODEL_64_48:
return "SH1106 64x48";
case SH1107_MODEL_128_64:
return "SH1107 128x64";
case SSD1305_MODEL_128_32:
return "SSD1305 128x32";
case SSD1305_MODEL_128_64:
return "SSD1305 128x64";
default:
return "Unknown";
}
} }
} // namespace ssd1306_base } // namespace ssd1306_base

View File

@@ -22,6 +22,9 @@ enum SSD1306Model {
SH1107_MODEL_128_128, SH1107_MODEL_128_128,
SSD1305_MODEL_128_32, SSD1305_MODEL_128_32,
SSD1305_MODEL_128_64, SSD1305_MODEL_128_64,
// When adding a new model, add it before SSD1306_MODEL_COUNT and update
// MODEL_DIMS and ModelStrings tables in ssd1306_base.cpp
SSD1306_MODEL_COUNT, // must be last
}; };
class SSD1306 : public display::DisplayBuffer { class SSD1306 : public display::DisplayBuffer {
@@ -70,7 +73,7 @@ class SSD1306 : public display::DisplayBuffer {
int get_height_internal() override; int get_height_internal() override;
int get_width_internal() override; int get_width_internal() override;
size_t get_buffer_length_(); size_t get_buffer_length_();
const char *model_str_(); const LogString *model_str_();
SSD1306Model model_{SSD1306_MODEL_128_64}; SSD1306Model model_{SSD1306_MODEL_128_64};
GPIOPin *reset_pin_{nullptr}; GPIOPin *reset_pin_{nullptr};

View File

@@ -28,7 +28,7 @@ void I2CSSD1306::dump_config() {
" Offset X: %d\n" " Offset X: %d\n"
" Offset Y: %d\n" " Offset Y: %d\n"
" Inverted Color: %s", " Inverted Color: %s",
this->model_str_(), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), LOG_STR_ARG(this->model_str_()), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_),
this->offset_x_, this->offset_y_, YESNO(this->invert_)); this->offset_x_, this->offset_y_, YESNO(this->invert_));
LOG_I2C_DEVICE(this); LOG_I2C_DEVICE(this);
LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_);

View File

@@ -24,7 +24,7 @@ void SPISSD1306::dump_config() {
" Offset X: %d\n" " Offset X: %d\n"
" Offset Y: %d\n" " Offset Y: %d\n"
" Inverted Color: %s", " Inverted Color: %s",
this->model_str_(), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), LOG_STR_ARG(this->model_str_()), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_),
this->offset_x_, this->offset_y_, YESNO(this->invert_)); this->offset_x_, this->offset_y_, YESNO(this->invert_));
LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_);

View File

@@ -344,14 +344,15 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
memcpy(user_info + user_len + 1, password, pass_len); memcpy(user_info + user_len + 1, password, pass_len);
user_info[user_info_len] = '\0'; user_info[user_info_len] = '\0';
size_t n = 0, out; // Base64 output size is ceil(input_len * 4/3) + 1, with input bounded to 256 bytes
esp_crypto_base64_encode(nullptr, 0, &n, reinterpret_cast<const uint8_t *>(user_info), user_info_len); // max output is ceil(256 * 4/3) + 1 = 343 bytes, use 350 for safety
constexpr size_t max_digest_len = 350;
auto digest = std::unique_ptr<char[]>(new char[n + 1]); char digest[max_digest_len];
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest.get()), n, &out, size_t out;
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
reinterpret_cast<const uint8_t *>(user_info), user_info_len); reinterpret_cast<const uint8_t *>(user_info), user_info_len);
return strcmp(digest.get(), auth_str + auth_prefix_len) == 0; return strcmp(digest, auth_str + auth_prefix_len) == 0;
} }
void AsyncWebServerRequest::requestAuthentication(const char *realm) const { void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
@@ -861,12 +862,12 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
} }
}); });
// Process data // Process data - use stack buffer to avoid heap allocation
std::unique_ptr<char[]> buffer(new char[MULTIPART_CHUNK_SIZE]); char buffer[MULTIPART_CHUNK_SIZE];
size_t bytes_since_yield = 0; size_t bytes_since_yield = 0;
for (size_t remaining = r->content_len; remaining > 0;) { for (size_t remaining = r->content_len; remaining > 0;) {
int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE)); int recv_len = httpd_req_recv(r, buffer, std::min(remaining, MULTIPART_CHUNK_SIZE));
if (recv_len <= 0) { if (recv_len <= 0) {
httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST, httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
@@ -874,7 +875,7 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL; return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
} }
if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) { if (reader->parse(buffer, recv_len) != static_cast<size_t>(recv_len)) {
ESP_LOGW(TAG, "Multipart parser error"); ESP_LOGW(TAG, "Multipart parser error");
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
return ESP_FAIL; return ESP_FAIL;

View File

@@ -133,6 +133,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
) )
# Suppress Python syntax warnings from third-party scripts during compilation # Suppress Python syntax warnings from third-party scripts during compilation
os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning")
# Increase uv retry count to handle transient network errors (default is 3)
os.environ.setdefault("UV_HTTP_RETRIES", "10")
cmd = ["platformio"] + list(args) cmd = ["platformio"] + list(args)
if not CORE.verbose: if not CORE.verbose:

View File

@@ -214,7 +214,7 @@ build_unflags =
; This are common settings for the LibreTiny (all variants) using Arduino. ; This are common settings for the LibreTiny (all variants) using Arduino.
[common:libretiny-arduino] [common:libretiny-arduino]
extends = common:arduino extends = common:arduino
platform = https://github.com/libretiny-eu/libretiny.git#v1.11.0 platform = https://github.com/libretiny-eu/libretiny.git#v1.12.1
framework = arduino framework = arduino
lib_compat_mode = soft lib_compat_mode = soft
lib_deps = lib_deps =

View File

@@ -6,8 +6,8 @@ esp32:
type: esp-idf type: esp-idf
components: components:
- espressif/mdns^1.8.2 - espressif/mdns^1.8.2
- name: espressif/esp_hosted - name: espressif/button
ref: 2.7.0 ref: 4.1.5
advanced: advanced:
enable_idf_experimental_features: yes enable_idf_experimental_features: yes
disable_debug_stubs: true disable_debug_stubs: true