diff --git a/Doxyfile b/Doxyfile index aa6a2f169e..4b38bb779e 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.1.0b2 +PROJECT_NUMBER = 2026.1.0b3 # 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 diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a63d33f73b..ed97c3b9a2 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -558,8 +558,10 @@ bool APIServer::clear_noise_psk(bool make_active) { #ifdef USE_HOMEASSISTANT_TIME void APIServer::request_time() { for (auto &client : this->clients_) { - if (!client->flags_.remove && client->is_authenticated()) + if (!client->flags_.remove && client->is_authenticated()) { client->send_time_request(); + return; // Only request from one client to avoid clock conflicts + } } } #endif diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index a1b684abbf..13f2fa59bd 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -44,7 +44,7 @@ void DallasTemperatureSensor::update() { this->send_command_(DALLAS_COMMAND_START_CONVERSION); - this->set_timeout(this->get_address_name(), this->millis_to_wait_for_conversion_(), [this] { + this->set_timeout(this->get_address_name().c_str(), this->millis_to_wait_for_conversion_(), [this] { if (!this->read_scratch_pad_() || !this->check_scratch_pad_()) { this->publish_state(NAN); return; diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 149fcc79d5..01f79156a9 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -193,10 +193,18 @@ void BLEClientBase::log_event_(const char *name) { ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name); } -void BLEClientBase::log_gattc_event_(const char *name) { +void BLEClientBase::log_gattc_lifecycle_event_(const char *name) { ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name); } +void BLEClientBase::log_gattc_data_event_(const char *name) { + // Data transfer events are logged at VERBOSE level because logging to UART creates + // delays that cause timing issues during time-sensitive BLE operations. This is + // especially problematic during pairing or firmware updates which require rapid + // writes to many characteristics - the log spam can cause these operations to fail. + ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name); +} + void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) { ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, status); } @@ -280,7 +288,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_OPEN_EVT: { if (!this->check_addr(param->open.remote_bda)) return false; - this->log_gattc_event_("OPEN"); + this->log_gattc_lifecycle_event_("OPEN"); // conn_id was already set in ESP_GATTC_CONNECT_EVT this->service_count_ = 0; @@ -331,7 +339,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_CONNECT_EVT: { if (!this->check_addr(param->connect.remote_bda)) return false; - this->log_gattc_event_("CONNECT"); + this->log_gattc_lifecycle_event_("CONNECT"); this->conn_id_ = param->connect.conn_id; // Start MTU negotiation immediately as recommended by ESP-IDF examples // (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in @@ -376,7 +384,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_CLOSE_EVT: { if (this->conn_id_ != param->close.conn_id) return false; - this->log_gattc_event_("CLOSE"); + this->log_gattc_lifecycle_event_("CLOSE"); this->release_services(); this->set_state(espbt::ClientState::IDLE); this->conn_id_ = UNSET_CONN_ID; @@ -404,7 +412,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_SEARCH_CMPL_EVT: { if (this->conn_id_ != param->search_cmpl.conn_id) return false; - this->log_gattc_event_("SEARCH_CMPL"); + this->log_gattc_lifecycle_event_("SEARCH_CMPL"); // For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery // This balances performance with bandwidth usage after the critical discovery phase if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { @@ -431,35 +439,35 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_READ_DESCR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_gattc_event_("READ_DESCR"); + this->log_gattc_data_event_("READ_DESCR"); break; } case ESP_GATTC_WRITE_DESCR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_gattc_event_("WRITE_DESCR"); + this->log_gattc_data_event_("WRITE_DESCR"); break; } case ESP_GATTC_WRITE_CHAR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_gattc_event_("WRITE_CHAR"); + this->log_gattc_data_event_("WRITE_CHAR"); break; } case ESP_GATTC_READ_CHAR_EVT: { if (this->conn_id_ != param->read.conn_id) return false; - this->log_gattc_event_("READ_CHAR"); + this->log_gattc_data_event_("READ_CHAR"); break; } case ESP_GATTC_NOTIFY_EVT: { if (this->conn_id_ != param->notify.conn_id) return false; - this->log_gattc_event_("NOTIFY"); + this->log_gattc_data_event_("NOTIFY"); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - this->log_gattc_event_("REG_FOR_NOTIFY"); + this->log_gattc_data_event_("REG_FOR_NOTIFY"); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { // Client is responsible for flipping the descriptor value @@ -491,7 +499,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ esp_err_t status = esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en), (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); - ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties); + ESP_LOGV(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties); if (status) { this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status); } @@ -499,13 +507,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { - this->log_gattc_event_("UNREG_FOR_NOTIFY"); + this->log_gattc_data_event_("UNREG_FOR_NOTIFY"); break; } default: - // ideally would check all other events for matching conn_id - ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event); + // Unknown events logged at VERBOSE to avoid UART delays during time-sensitive operations + ESP_LOGV(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event); break; } return true; diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 92c7444ee1..c52f0e5d2d 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -127,7 +127,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { // 6 bytes used, 2 bytes padding void log_event_(const char *name); - void log_gattc_event_(const char *name); + void log_gattc_lifecycle_event_(const char *name); + void log_gattc_data_event_(const char *name); void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, const char *param_type); void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, diff --git a/esphome/components/hmac_sha256/hmac_sha256.cpp b/esphome/components/hmac_sha256/hmac_sha256.cpp index cf5daf63af..2146e961bc 100644 --- a/esphome/components/hmac_sha256/hmac_sha256.cpp +++ b/esphome/components/hmac_sha256/hmac_sha256.cpp @@ -1,4 +1,3 @@ -#include #include #include "hmac_sha256.h" #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST) @@ -26,9 +25,7 @@ void HmacSHA256::calculate() { mbedtls_md_hmac_finish(&this->ctx_, this->digest_ void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); } void HmacSHA256::get_hex(char *output) { - for (size_t i = 0; i < SHA256_DIGEST_SIZE; i++) { - sprintf(output + (i * 2), "%02x", this->digest_[i]); - } + format_hex_to(output, SHA256_DIGEST_SIZE * 2 + 1, this->digest_, SHA256_DIGEST_SIZE); } bool HmacSHA256::equals_bytes(const uint8_t *expected) { diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 1b5fd9f00e..a8c2cdfc63 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -242,9 +242,7 @@ template class HttpRequestSendAction : public Action { return; } - size_t content_length = container->content_length; - size_t max_length = std::min(content_length, this->max_response_buffer_size_); - + size_t max_length = this->max_response_buffer_size_; #ifdef USE_HTTP_REQUEST_RESPONSE if (this->capture_response_.value(x...)) { std::string response_body; diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 725a9c1c1e..1de947ba5b 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -213,18 +213,12 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); - int bufsize = std::min(max_len, this->content_length - this->bytes_read_); - - if (bufsize == 0) { - this->duration_ms += (millis() - start); - return 0; + this->feed_wdt(); + int read_len = esp_http_client_read(this->client_, (char *) buf, max_len); + this->feed_wdt(); + if (read_len > 0) { + this->bytes_read_ += read_len; } - - this->feed_wdt(); - int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize); - this->feed_wdt(); - this->bytes_read_ += read_len; - this->duration_ms += (millis() - start); return read_len; diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index 20c731e730..0eeb4bba33 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -1,3 +1,4 @@ +import logging from typing import Any from esphome import automation, pins @@ -18,13 +19,16 @@ from esphome.const import ( CONF_ROTATION, CONF_UPDATE_INTERVAL, ) -from esphome.core import ID +from esphome.core import ID, EnumValue from esphome.cpp_generator import MockObj, TemplateArgsType import esphome.final_validate as fv +from esphome.helpers import add_class_to_obj from esphome.types import ConfigType from . import boards, hub75_ns +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ["esp32"] CODEOWNERS = ["@stuartparmenter"] @@ -120,13 +124,51 @@ PANEL_LAYOUTS = { } Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True) -SCAN_PATTERNS = { +SCAN_WIRINGS = { "STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN, - "FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH, - "FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH, - "FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH, + "SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH, + "SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH, + "SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH, + "SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH, } +# Deprecated scan wiring names - mapped to new names +DEPRECATED_SCAN_WIRINGS = { + "FOUR_SCAN_16PX_HIGH": "SCAN_1_4_16PX_HIGH", + "FOUR_SCAN_32PX_HIGH": "SCAN_1_8_32PX_HIGH", + "FOUR_SCAN_64PX_HIGH": "SCAN_1_8_64PX_HIGH", +} + + +def _validate_scan_wiring(value): + """Validate scan_wiring with deprecation warnings for old names.""" + value = cv.string(value).upper().replace(" ", "_") + + # Check if using deprecated name + # Remove deprecated names in 2026.7.0 + if value in DEPRECATED_SCAN_WIRINGS: + new_name = DEPRECATED_SCAN_WIRINGS[value] + _LOGGER.warning( + "Scan wiring '%s' is deprecated and will be removed in ESPHome 2026.7.0. " + "Please use '%s' instead.", + value, + new_name, + ) + value = new_name + + # Validate against allowed values + if value not in SCAN_WIRINGS: + raise cv.Invalid( + f"Unknown scan wiring '{value}'. " + f"Valid options are: {', '.join(sorted(SCAN_WIRINGS.keys()))}" + ) + + # Return as EnumValue like cv.enum does + result = add_class_to_obj(value, EnumValue) + result.enum_value = SCAN_WIRINGS[value] + return result + + Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True) CLOCK_SPEEDS = { "8MHZ": Hub75ClockSpeed.HZ_8M, @@ -382,9 +424,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_LAYOUT_COLS): cv.positive_int, cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"), # Panel hardware configuration - cv.Optional(CONF_SCAN_WIRING): cv.enum( - SCAN_PATTERNS, upper=True, space="_" - ), + cv.Optional(CONF_SCAN_WIRING): _validate_scan_wiring, cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True), # Display configuration cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean, @@ -547,7 +587,7 @@ def _build_config_struct( async def to_code(config: ConfigType) -> None: add_idf_component( name="esphome/esp-hub75", - ref="0.2.2", + ref="0.3.0", ) # Set compile-time configuration via build flags (so external library sees them) diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp index b08f84029b..cc500ba429 100644 --- a/esphome/components/ntc/ntc.cpp +++ b/esphome/components/ntc/ntc.cpp @@ -23,7 +23,7 @@ void NTC::process_(float value) { double v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr; auto temp = float(1.0 / v - 273.15); - ESP_LOGD(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp); + ESP_LOGV(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp); this->publish_state(temp); } diff --git a/esphome/components/resistance/resistance_sensor.cpp b/esphome/components/resistance/resistance_sensor.cpp index 6e57214449..706a059de3 100644 --- a/esphome/components/resistance/resistance_sensor.cpp +++ b/esphome/components/resistance/resistance_sensor.cpp @@ -39,7 +39,7 @@ void ResistanceSensor::process_(float value) { } res *= this->resistor_; - ESP_LOGD(TAG, "'%s' - Resistance %.1fΩ", this->name_.c_str(), res); + ESP_LOGV(TAG, "'%s' - Resistance %.1fΩ", this->name_.c_str(), res); this->publish_state(res); } diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index ca9f85abd8..2813b4450b 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -332,6 +332,7 @@ Sprinkler::Sprinkler(const std::string &name) { // The `name` is needed to set timers up, hence non-default constructor // replaces `set_name()` method previously existed this->name_ = name; + this->timer_.init(2); this->timer_.push_back({this->name_ + "sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)}); this->timer_.push_back({this->name_ + "vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}); } @@ -1574,7 +1575,8 @@ const LogString *Sprinkler::state_as_str_(SprinklerState state) { void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { if (this->timer_duration_(timer_index) > 0) { - this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index), + // FixedVector ensures timer_ can't be resized, so .c_str() pointers remain valid + this->set_timeout(this->timer_[timer_index].name.c_str(), this->timer_duration_(timer_index), this->timer_cbf_(timer_index)); this->timer_[timer_index].start_time = millis(); this->timer_[timer_index].active = true; @@ -1585,7 +1587,7 @@ void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) { this->timer_[timer_index].active = false; - return this->cancel_timeout(this->timer_[timer_index].name); + return this->cancel_timeout(this->timer_[timer_index].name.c_str()); } bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; } diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 25e2d42446..273c0e9208 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -3,6 +3,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/number/number.h" #include "esphome/components/switch/switch.h" @@ -553,8 +554,8 @@ class Sprinkler : public Component { /// Sprinkler valve operator objects std::vector valve_op_{2}; - /// Valve control timers - std::vector timer_{}; + /// Valve control timers - FixedVector enforces that this can never grow beyond init() size + FixedVector timer_; /// Other Sprinkler instances we should be aware of (used to check if pumps are in use) std::vector other_controllers_; diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 639af4457f..f217d14c55 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -31,6 +31,18 @@ void RealTimeClock::dump_config() { void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); + // Skip if time is already synchronized to avoid unnecessary writes, log spam, + // and prevent clock jumping backwards due to network latency + constexpr time_t min_valid_epoch = 1546300800; // January 1, 2019 + time_t current_time = this->timestamp_now(); + // Check if time is valid (year >= 2019) before comparing + if (current_time >= min_valid_epoch) { + // Unsigned subtraction handles wraparound correctly, then cast to signed + int32_t diff = static_cast(epoch - static_cast(current_time)); + if (diff >= -1 && diff <= 1) { + return; + } + } // Update UTC epoch time. #ifdef USE_ZEPHYR struct timespec ts; diff --git a/esphome/const.py b/esphome/const.py index e99fbb8283..35ce8b3cd6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.1.0b2" +__version__ = "2026.1.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 045b3f9168..5903e68e8e 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -28,8 +28,8 @@ dependencies: rules: - if: "target in [esp32s2, esp32s3, esp32p4]" esphome/esp-hub75: - version: 0.2.2 + version: 0.3.0 rules: - - if: "target in [esp32, esp32s2, esp32s3, esp32p4]" + - if: "target in [esp32, esp32s2, esp32s3, esp32c6, esp32p4]" esp32async/asynctcp: version: 3.4.91