diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 0a272d21ba..009f9db388 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce +d565b0589e35e692b5f2fc0c14723a99595b4828a3a3ef96c442e86a23176c00 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 15edd8421a..be761cee3d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 + uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 + uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 with: category: "/language:${{matrix.language}}" diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 97b8c2213e..68688794d7 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -1,11 +1,11 @@ #include "cover.h" #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" +#include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include -#include "esphome/core/log.h" - namespace esphome::cover { static const char *const TAG = "cover"; @@ -39,13 +39,13 @@ Cover::Cover() : position{COVER_OPEN} {} CoverCall::CoverCall(Cover *parent) : parent_(parent) {} CoverCall &CoverCall::set_command(const char *command) { - if (strcasecmp(command, "OPEN") == 0) { + if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("OPEN")) == 0) { this->set_command_open(); - } else if (strcasecmp(command, "CLOSE") == 0) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLOSE")) == 0) { this->set_command_close(); - } else if (strcasecmp(command, "STOP") == 0) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) { this->set_command_stop(); - } else if (strcasecmp(command, "TOGGLE") == 0) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) { this->set_command_toggle(); } else { ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command); diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 9f74fb1023..7347b8ebf7 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -126,7 +126,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_CA_CERTIFICATE_PATH): cv.All( cv.file_, - cv.only_on(PLATFORM_HOST), + cv.Any(cv.only_on(PLATFORM_HOST), cv.only_on_esp32), ), } ).extend(cv.COMPONENT_SCHEMA), @@ -160,7 +160,14 @@ async def to_code(config): cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL])) if config.get(CONF_VERIFY_SSL): - esp32.add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) + if ca_cert_path := config.get(CONF_CA_CERTIFICATE_PATH): + with open(ca_cert_path, encoding="utf-8") as f: + ca_cert_content = f.read() + cg.add(var.set_ca_certificate(ca_cert_content)) + else: + esp32.add_idf_sdkconfig_option( + "CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True + ) esp32.add_idf_sdkconfig_option( "CONFIG_ESP_TLS_INSECURE", diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index b6fb7f7ea9..680ae6c801 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -27,8 +27,9 @@ void HttpRequestIDF::dump_config() { HttpRequestComponent::dump_config(); ESP_LOGCONFIG(TAG, " Buffer Size RX: %u\n" - " Buffer Size TX: %u", - this->buffer_size_rx_, this->buffer_size_tx_); + " Buffer Size TX: %u\n" + " Custom CA Certificate: %s", + this->buffer_size_rx_, this->buffer_size_tx_, YESNO(this->ca_certificate_ != nullptr)); } esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { @@ -88,11 +89,15 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c config.disable_auto_redirect = !this->follow_redirects_; config.max_redirection_count = this->redirect_limit_; config.auth_type = HTTP_AUTH_TYPE_BASIC; -#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE if (secure && this->verify_ssl_) { - config.crt_bundle_attach = esp_crt_bundle_attach; - } + if (this->ca_certificate_ != nullptr) { + config.cert_pem = this->ca_certificate_; +#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE + } else { + config.crt_bundle_attach = esp_crt_bundle_attach; #endif + } + } if (this->useragent_ != nullptr) { config.user_agent = this->useragent_; diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 0fae67f5bc..ad11811a8f 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -35,6 +35,7 @@ class HttpRequestIDF : public HttpRequestComponent { void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; } void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; } void set_verify_ssl(bool verify_ssl) { this->verify_ssl_ = verify_ssl; } + void set_ca_certificate(const char *ca_certificate) { this->ca_certificate_ = ca_certificate; } protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, @@ -44,6 +45,7 @@ class HttpRequestIDF : public HttpRequestComponent { uint16_t buffer_size_rx_{}; uint16_t buffer_size_tx_{}; bool verify_ssl_{true}; + const char *ca_certificate_{nullptr}; /// @brief Monitors the http client events to gather response headers static esp_err_t http_event_handler(esp_http_client_event_t *evt); diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 6c77e75d8c..8a4b3684cf 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -82,7 +82,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() { uint32_t last_progress = 0; uint32_t update_start_time = millis(); md5::MD5Digest md5_receive; - std::unique_ptr md5_receive_str(new char[33]); + char md5_receive_str[33]; if (this->md5_expected_.empty() && !this->http_get_md5_()) { return OTA_MD5_INVALID; @@ -176,14 +176,14 @@ uint8_t OtaHttpRequestComponent::do_ota_() { // verify MD5 is as expected and act accordingly md5_receive.calculate(); - md5_receive.get_hex(md5_receive_str.get()); - this->md5_computed_ = md5_receive_str.get(); + md5_receive.get_hex(md5_receive_str); + this->md5_computed_ = md5_receive_str; if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) { ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str()); this->cleanup_(std::move(backend), container); return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH; } else { - backend->set_update_md5(md5_receive_str.get()); + backend->set_update_md5(md5_receive_str); } container->end(); diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 503ec7e167..553179beec 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -191,10 +191,17 @@ def _notify_old_style(config): # The dev and latest branches will be at *least* this version, which is what matters. +# Use GitHub releases directly to avoid PlatformIO moderation delays. ARDUINO_VERSIONS = { - "dev": (cv.Version(1, 9, 2), "https://github.com/libretiny-eu/libretiny.git"), - "latest": (cv.Version(1, 9, 2), "libretiny"), - "recommended": (cv.Version(1, 9, 2), None), + "dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"), + "latest": ( + cv.Version(1, 11, 0), + "https://github.com/libretiny-eu/libretiny.git#v1.11.0", + ), + "recommended": ( + cv.Version(1, 11, 0), + "https://github.com/libretiny-eu/libretiny.git#v1.11.0", + ), } diff --git a/esphome/components/light/addressable_light_wrapper.h b/esphome/components/light/addressable_light_wrapper.h index 8665e62a79..cd83482248 100644 --- a/esphome/components/light/addressable_light_wrapper.h +++ b/esphome/components/light/addressable_light_wrapper.h @@ -7,9 +7,7 @@ namespace esphome::light { class AddressableLightWrapper : public light::AddressableLight { public: - explicit AddressableLightWrapper(light::LightState *light_state) : light_state_(light_state) { - this->wrapper_state_ = new uint8_t[5]; // NOLINT(cppcoreguidelines-owning-memory) - } + explicit AddressableLightWrapper(light::LightState *light_state) : light_state_(light_state) {} int32_t size() const override { return 1; } @@ -118,7 +116,7 @@ class AddressableLightWrapper : public light::AddressableLight { } light::LightState *light_state_; - uint8_t *wrapper_state_; + mutable uint8_t wrapper_state_[5]{}; ColorMode color_mode_{ColorMode::UNKNOWN}; }; diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index 259d597b44..b6b4031e16 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -3,8 +3,7 @@ #include -namespace esphome { -namespace mhz19 { +namespace esphome::mhz19 { static const char *const TAG = "mhz19"; static const uint8_t MHZ19_REQUEST_LENGTH = 8; @@ -17,6 +16,19 @@ static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM[] = {0xFF, 0x01, 0x static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x13, 0x88}; static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x27, 0x10}; +static const LogString *detection_range_to_log_string(MHZ19DetectionRange range) { + switch (range) { + case MHZ19_DETECTION_RANGE_0_2000PPM: + return LOG_STR("0-2000 ppm"); + case MHZ19_DETECTION_RANGE_0_5000PPM: + return LOG_STR("0-5000 ppm"); + case MHZ19_DETECTION_RANGE_0_10000PPM: + return LOG_STR("0-10000 ppm"); + default: + return LOG_STR("default"); + } +} + uint8_t mhz19_checksum(const uint8_t *command) { uint8_t sum = 0; for (uint8_t i = 1; i < MHZ19_REQUEST_LENGTH; i++) { @@ -91,24 +103,24 @@ void MHZ19Component::abc_disable() { this->mhz19_write_command_(MHZ19_COMMAND_ABC_DISABLE, nullptr); } -void MHZ19Component::range_set(MHZ19DetectionRange detection_ppm) { - switch (detection_ppm) { - case MHZ19_DETECTION_RANGE_DEFAULT: - ESP_LOGV(TAG, "Using previously set detection range (no change)"); - break; +void MHZ19Component::range_set(MHZ19DetectionRange detection_range) { + const uint8_t *command; + switch (detection_range) { case MHZ19_DETECTION_RANGE_0_2000PPM: - ESP_LOGD(TAG, "Setting detection range to 0 to 2000ppm"); - this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM, nullptr); + command = MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM; break; case MHZ19_DETECTION_RANGE_0_5000PPM: - ESP_LOGD(TAG, "Setting detection range to 0 to 5000ppm"); - this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM, nullptr); + command = MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM; break; case MHZ19_DETECTION_RANGE_0_10000PPM: - ESP_LOGD(TAG, "Setting detection range to 0 to 10000ppm"); - this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM, nullptr); + command = MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM; break; + default: + ESP_LOGV(TAG, "Using previously set detection range (no change)"); + return; } + ESP_LOGD(TAG, "Setting detection range to %s", LOG_STR_ARG(detection_range_to_log_string(detection_range))); + this->mhz19_write_command_(command, nullptr); } bool MHZ19Component::mhz19_write_command_(const uint8_t *command, uint8_t *response) { @@ -140,27 +152,7 @@ void MHZ19Component::dump_config() { } ESP_LOGCONFIG(TAG, " Warmup time: %" PRIu32 " s", this->warmup_seconds_); - - const char *range_str; - switch (this->detection_range_) { - case MHZ19_DETECTION_RANGE_DEFAULT: - range_str = "default"; - break; - case MHZ19_DETECTION_RANGE_0_2000PPM: - range_str = "0 to 2000ppm"; - break; - case MHZ19_DETECTION_RANGE_0_5000PPM: - range_str = "0 to 5000ppm"; - break; - case MHZ19_DETECTION_RANGE_0_10000PPM: - range_str = "0 to 10000ppm"; - break; - default: - range_str = "default"; - break; - } - ESP_LOGCONFIG(TAG, " Detection range: %s", range_str); + ESP_LOGCONFIG(TAG, " Detection range: %s", LOG_STR_ARG(detection_range_to_log_string(this->detection_range_))); } -} // namespace mhz19 -} // namespace esphome +} // namespace esphome::mhz19 diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index 5898bab649..a27f1c31eb 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace mhz19 { +namespace esphome::mhz19 { enum MHZ19ABCLogic { MHZ19_ABC_NONE = 0, @@ -32,7 +31,7 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice { void calibrate_zero(); void abc_enable(); void abc_disable(); - void range_set(MHZ19DetectionRange detection_ppm); + void range_set(MHZ19DetectionRange detection_range); void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } @@ -74,5 +73,4 @@ template class MHZ19DetectionRangeSetAction : public Actionparent_->range_set(this->detection_range_.value(x...)); } }; -} // namespace mhz19 -} // namespace esphome +} // namespace esphome::mhz19 diff --git a/esphome/components/sml/constants.h b/esphome/components/sml/constants.h index d6761d4bb7..0142fe98f7 100644 --- a/esphome/components/sml/constants.h +++ b/esphome/components/sml/constants.h @@ -1,5 +1,6 @@ #pragma once +#include #include namespace esphome { @@ -21,7 +22,7 @@ enum SmlMessageType : uint16_t { SML_PUBLIC_OPEN_RES = 0x0101, SML_GET_LIST_RES const uint16_t START_MASK = 0x55aa; // 0x1b 1b 1b 1b 01 01 01 01 const uint16_t END_MASK = 0x0157; // 0x1b 1b 1b 1b 1a -const std::vector START_SEQ = {0x1b, 0x1b, 0x1b, 0x1b, 0x01, 0x01, 0x01, 0x01}; +constexpr std::array START_SEQ = {0x1b, 0x1b, 0x1b, 0x1b, 0x01, 0x01, 0x01, 0x01}; } // namespace sml } // namespace esphome diff --git a/esphome/components/tm1638/tm1638.cpp b/esphome/components/tm1638/tm1638.cpp index 7ba63fe218..8ef546ff32 100644 --- a/esphome/components/tm1638/tm1638.cpp +++ b/esphome/components/tm1638/tm1638.cpp @@ -35,9 +35,6 @@ void TM1638Component::setup() { this->set_intensity(intensity_); this->reset_(); // all LEDs off - - for (uint8_t i = 0; i < 8; i++) // zero fill print buffer - this->buffer_[i] = 0; } void TM1638Component::dump_config() { diff --git a/esphome/components/tm1638/tm1638.h b/esphome/components/tm1638/tm1638.h index f6b2922ecf..27898aa3dc 100644 --- a/esphome/components/tm1638/tm1638.h +++ b/esphome/components/tm1638/tm1638.h @@ -70,7 +70,7 @@ class TM1638Component : public PollingComponent { GPIOPin *clk_pin_; GPIOPin *stb_pin_; GPIOPin *dio_pin_; - uint8_t *buffer_ = new uint8_t[8]; + uint8_t buffer_[8]{}; tm1638_writer_t writer_{}; std::vector listeners_{}; }; diff --git a/esphome/components/uln2003/uln2003.cpp b/esphome/components/uln2003/uln2003.cpp index 11e1c3d4c0..a2244eadaa 100644 --- a/esphome/components/uln2003/uln2003.cpp +++ b/esphome/components/uln2003/uln2003.cpp @@ -1,11 +1,23 @@ #include "uln2003.h" #include "esphome/core/log.h" -namespace esphome { -namespace uln2003 { +namespace esphome::uln2003 { static const char *const TAG = "uln2003.stepper"; +static const LogString *step_mode_to_log_string(ULN2003StepMode mode) { + switch (mode) { + case ULN2003_STEP_MODE_FULL_STEP: + return LOG_STR("FULL STEP"); + case ULN2003_STEP_MODE_HALF_STEP: + return LOG_STR("HALF STEP"); + case ULN2003_STEP_MODE_WAVE_DRIVE: + return LOG_STR("WAVE DRIVE"); + default: + return LOG_STR("UNKNOWN"); + } +} + void ULN2003::setup() { this->pin_a_->setup(); this->pin_b_->setup(); @@ -42,22 +54,7 @@ void ULN2003::dump_config() { LOG_PIN(" Pin B: ", this->pin_b_); LOG_PIN(" Pin C: ", this->pin_c_); LOG_PIN(" Pin D: ", this->pin_d_); - const char *step_mode_s; - switch (this->step_mode_) { - case ULN2003_STEP_MODE_FULL_STEP: - step_mode_s = "FULL STEP"; - break; - case ULN2003_STEP_MODE_HALF_STEP: - step_mode_s = "HALF STEP"; - break; - case ULN2003_STEP_MODE_WAVE_DRIVE: - step_mode_s = "WAVE DRIVE"; - break; - default: - step_mode_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Step Mode: %s", step_mode_s); + ESP_LOGCONFIG(TAG, " Step Mode: %s", LOG_STR_ARG(step_mode_to_log_string(this->step_mode_))); } void ULN2003::write_step_(int32_t step) { int32_t n = this->step_mode_ == ULN2003_STEP_MODE_HALF_STEP ? 8 : 4; @@ -90,5 +87,4 @@ void ULN2003::write_step_(int32_t step) { this->pin_d_->digital_write((res >> 3) & 1); } -} // namespace uln2003 -} // namespace esphome +} // namespace esphome::uln2003 diff --git a/esphome/components/uln2003/uln2003.h b/esphome/components/uln2003/uln2003.h index 4f559ed9a0..70f55f72bf 100644 --- a/esphome/components/uln2003/uln2003.h +++ b/esphome/components/uln2003/uln2003.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/stepper/stepper.h" -namespace esphome { -namespace uln2003 { +namespace esphome::uln2003 { enum ULN2003StepMode { ULN2003_STEP_MODE_FULL_STEP, @@ -40,5 +39,4 @@ class ULN2003 : public stepper::Stepper, public Component { int32_t current_uln_pos_{0}; }; -} // namespace uln2003 -} // namespace esphome +} // namespace esphome::uln2003 diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index a9086747ce..3a7c3cbf88 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -2,6 +2,8 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" + #include namespace esphome { @@ -38,13 +40,13 @@ Valve::Valve() : position{VALVE_OPEN} {} ValveCall::ValveCall(Valve *parent) : parent_(parent) {} ValveCall &ValveCall::set_command(const char *command) { - if (strcasecmp(command, "OPEN") == 0) { + if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("OPEN")) == 0) { this->set_command_open(); - } else if (strcasecmp(command, "CLOSE") == 0) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLOSE")) == 0) { this->set_command_close(); - } else if (strcasecmp(command, "STOP") == 0) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) { this->set_command_stop(); - } else if (strcasecmp(command, "TOGGLE") == 0) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) { this->set_command_toggle(); } else { ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command); diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index e7b901d71f..1a5d22f8d8 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -3,6 +3,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/string_ref.h" #include @@ -451,15 +452,15 @@ std::string format_bin(const uint8_t *data, size_t length) { } ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { - if (on == nullptr && strcasecmp(str, "on") == 0) + if (on == nullptr && ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("on")) == 0) return PARSE_ON; if (on != nullptr && strcasecmp(str, on) == 0) return PARSE_ON; - if (off == nullptr && strcasecmp(str, "off") == 0) + if (off == nullptr && ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("off")) == 0) return PARSE_OFF; if (off != nullptr && strcasecmp(str, off) == 0) return PARSE_OFF; - if (strcasecmp(str, "toggle") == 0) + if (ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("toggle")) == 0) return PARSE_TOGGLE; return PARSE_NONE; diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 6c3e4cec96..4b897fb2de 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -12,6 +12,10 @@ #define ESPHOME_strncpy_P strncpy_P #define ESPHOME_strncat_P strncat_P #define ESPHOME_snprintf_P snprintf_P +#define ESPHOME_strcmp_P strcmp_P +#define ESPHOME_strcasecmp_P strcasecmp_P +#define ESPHOME_strncmp_P strncmp_P +#define ESPHOME_strncasecmp_P strncasecmp_P // Type for pointers to PROGMEM strings (for use with ESPHOME_F return values) using ProgmemStr = const __FlashStringHelper *; #else @@ -21,6 +25,10 @@ using ProgmemStr = const __FlashStringHelper *; #define ESPHOME_strncpy_P strncpy #define ESPHOME_strncat_P strncat #define ESPHOME_snprintf_P snprintf +#define ESPHOME_strcmp_P strcmp +#define ESPHOME_strcasecmp_P strcasecmp +#define ESPHOME_strncmp_P strncmp +#define ESPHOME_strncasecmp_P strncasecmp // Type for pointers to strings (no PROGMEM on non-ESP8266 platforms) using ProgmemStr = const char *; #endif diff --git a/esphome/espota2.py b/esphome/espota2.py index 95dd602ad2..2d90251b38 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -154,6 +154,12 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None """ if not expect: return + if not data: + raise OTAError( + "Error: Device closed connection without responding. " + "This may indicate the device ran out of memory, " + "a network issue, or the connection was interrupted." + ) dat = data[0] if dat == RESPONSE_ERROR_MAGIC: raise OTAError("Error: Invalid magic byte") diff --git a/platformio.ini b/platformio.ini index e9a588e4fd..9de72cd622 100644 --- a/platformio.ini +++ b/platformio.ini @@ -212,7 +212,7 @@ build_unflags = ; This are common settings for the LibreTiny (all variants) using Arduino. [common:libretiny-arduino] extends = common:arduino -platform = libretiny@1.9.2 +platform = https://github.com/libretiny-eu/libretiny.git#v1.11.0 framework = arduino lib_compat_mode = soft lib_deps = diff --git a/pyproject.toml b/pyproject.toml index 3ce2e6ebec..1bd43ea2f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==80.10.1", "wheel>=0.43,<0.47"] +requires = ["setuptools==80.10.2", "wheel>=0.43,<0.47"] build-backend = "setuptools.build_meta" [project] diff --git a/requirements.txt b/requirements.txt index a707fda059..821324262b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20260110.0 -aioesphomeapi==43.13.0 +aioesphomeapi==43.14.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import diff --git a/tests/components/http_request/test-custom-ca.esp32-idf.yaml b/tests/components/http_request/test-custom-ca.esp32-idf.yaml new file mode 100644 index 0000000000..0b1b2f8829 --- /dev/null +++ b/tests/components/http_request/test-custom-ca.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + verify_ssl: "true" + +http_request: + ca_certificate_path: $component_dir/test_ca.pem + +<<: !include common.yaml diff --git a/tests/components/http_request/test_ca.pem b/tests/components/http_request/test_ca.pem new file mode 100644 index 0000000000..30cbc1a3c4 --- /dev/null +++ b/tests/components/http_request/test_ca.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBkTCB+wIJAKHBfpegPjMCMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBnVu +dXNlZDAeFw0yNDAxMDEwMDAwMDBaFw0yNTAxMDEwMDAwMDBaMBExDzANBgNVBAMM +BnVudXNlZDBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC5mMUB1hOgLmlnXtsvcGMP +XkhAqZaR0dDPW5OS8VEopWLJCX9Y0cvNCqiDI8cnP8pP8XJGU1hGLvA5PJzWnWZz +AgMBAAGjUzBRMB0GA1UdDgQWBBR5oQ9KqFeZOdBuAJrXxEP0dqzPtTAfBgNVHSME +GDAWgBR5oQ9KqFeZOdBuAJrXxEP0dqzPtTAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBCwUAA0EAKqZFf6+f8FPDbKyPCpssquojgn7fEXqr/I/yz0R5CowGdMms +H3WH3aKP4lLSHdPTBtfIoJi3gEIZjFxp3S1TWw== +-----END CERTIFICATE----- diff --git a/tests/components/text_sensor/common.yaml b/tests/components/text_sensor/common.yaml index 4459c0fa44..97b0b8ad94 100644 --- a/tests/components/text_sensor/common.yaml +++ b/tests/components/text_sensor/common.yaml @@ -64,3 +64,16 @@ text_sensor: - suffix -> SUFFIX - map: - PREFIX text SUFFIX -> mapped + + - platform: template + name: "Test Lambda Filter" + id: test_lambda_filter + filters: + - lambda: |- + return {"[" + x + "]"}; + - to_upper + - lambda: |- + if (x.length() > 10) { + return {x.substr(0, 10) + "..."}; + } + return {x}; diff --git a/tests/integration/fixtures/text_sensor_raw_state.yaml b/tests/integration/fixtures/text_sensor_raw_state.yaml index 54ab2e8dcc..a4b735e889 100644 --- a/tests/integration/fixtures/text_sensor_raw_state.yaml +++ b/tests/integration/fixtures/text_sensor_raw_state.yaml @@ -56,6 +56,36 @@ text_sensor: - prepend: "[" - append: "]" + - platform: template + name: "To Lower Sensor" + id: to_lower_sensor + filters: + - to_lower + + - platform: template + name: "Lambda Sensor" + id: lambda_sensor + filters: + - lambda: |- + return {"[" + x + "]"}; + + - platform: template + name: "Lambda Raw State Sensor" + id: lambda_raw_state_sensor + filters: + - lambda: |- + return {x + " MODIFIED"}; + + - platform: template + name: "Lambda Skip Sensor" + id: lambda_skip_sensor + filters: + - lambda: |- + if (x == "skip") { + return {}; + } + return {x + " passed"}; + # Button to publish values and log raw_state vs state button: - platform: template @@ -179,3 +209,73 @@ button: format: "CHAINED: state='%s'" args: - id(chained_sensor).state.c_str() + + - platform: template + name: "Test To Lower Button" + id: test_to_lower_button + on_press: + - text_sensor.template.publish: + id: to_lower_sensor + state: "HELLO WORLD" + - delay: 50ms + - logger.log: + format: "TO_LOWER: state='%s'" + args: + - id(to_lower_sensor).state.c_str() + + - platform: template + name: "Test Lambda Button" + id: test_lambda_button + on_press: + - text_sensor.template.publish: + id: lambda_sensor + state: "test" + - delay: 50ms + - logger.log: + format: "LAMBDA: state='%s'" + args: + - id(lambda_sensor).state.c_str() + + - platform: template + name: "Test Lambda Pass Button" + id: test_lambda_pass_button + on_press: + - text_sensor.template.publish: + id: lambda_skip_sensor + state: "value" + - delay: 50ms + - logger.log: + format: "LAMBDA_PASS: state='%s'" + args: + - id(lambda_skip_sensor).state.c_str() + + - platform: template + name: "Test Lambda Skip Button" + id: test_lambda_skip_button + on_press: + - text_sensor.template.publish: + id: lambda_skip_sensor + state: "skip" + - delay: 50ms + # When lambda returns {}, the value should NOT be published + # so state should remain from previous publish (or empty if first) + - logger.log: + format: "LAMBDA_SKIP: state='%s'" + args: + - id(lambda_skip_sensor).state.c_str() + + - platform: template + name: "Test Lambda Raw State Button" + id: test_lambda_raw_state_button + on_press: + - text_sensor.template.publish: + id: lambda_raw_state_sensor + state: "original" + - delay: 50ms + # Verify raw_state is preserved (not mutated) after lambda filter + # state should be "original MODIFIED", raw_state should be "original" + - logger.log: + format: "LAMBDA_RAW_STATE: state='%s' raw_state='%s'" + args: + - id(lambda_raw_state_sensor).state.c_str() + - id(lambda_raw_state_sensor).get_raw_state().c_str() diff --git a/tests/integration/test_text_sensor_raw_state.py b/tests/integration/test_text_sensor_raw_state.py index 482ebbe9c2..476dd2713e 100644 --- a/tests/integration/test_text_sensor_raw_state.py +++ b/tests/integration/test_text_sensor_raw_state.py @@ -42,6 +42,11 @@ async def test_text_sensor_raw_state( map_off_future: asyncio.Future[str] = loop.create_future() map_unknown_future: asyncio.Future[str] = loop.create_future() chained_future: asyncio.Future[str] = loop.create_future() + to_lower_future: asyncio.Future[str] = loop.create_future() + lambda_future: asyncio.Future[str] = loop.create_future() + lambda_pass_future: asyncio.Future[str] = loop.create_future() + lambda_skip_future: asyncio.Future[str] = loop.create_future() + lambda_raw_state_future: asyncio.Future[tuple[str, str]] = loop.create_future() # Patterns to match log output # NO_FILTER: state='hello world' raw_state='hello world' @@ -58,6 +63,13 @@ async def test_text_sensor_raw_state( map_off_pattern = re.compile(r"MAP_OFF: state='([^']*)'") map_unknown_pattern = re.compile(r"MAP_UNKNOWN: state='([^']*)'") chained_pattern = re.compile(r"CHAINED: state='([^']*)'") + to_lower_pattern = re.compile(r"TO_LOWER: state='([^']*)'") + lambda_pattern = re.compile(r"LAMBDA: state='([^']*)'") + lambda_pass_pattern = re.compile(r"LAMBDA_PASS: state='([^']*)'") + lambda_skip_pattern = re.compile(r"LAMBDA_SKIP: state='([^']*)'") + lambda_raw_state_pattern = re.compile( + r"LAMBDA_RAW_STATE: state='([^']*)' raw_state='([^']*)'" + ) def check_output(line: str) -> None: """Check log output for expected messages.""" @@ -92,6 +104,27 @@ async def test_text_sensor_raw_state( if not chained_future.done() and (match := chained_pattern.search(line)): chained_future.set_result(match.group(1)) + if not to_lower_future.done() and (match := to_lower_pattern.search(line)): + to_lower_future.set_result(match.group(1)) + + if not lambda_future.done() and (match := lambda_pattern.search(line)): + lambda_future.set_result(match.group(1)) + + if not lambda_pass_future.done() and ( + match := lambda_pass_pattern.search(line) + ): + lambda_pass_future.set_result(match.group(1)) + + if not lambda_skip_future.done() and ( + match := lambda_skip_pattern.search(line) + ): + lambda_skip_future.set_result(match.group(1)) + + if not lambda_raw_state_future.done() and ( + match := lambda_raw_state_pattern.search(line) + ): + lambda_raw_state_future.set_result((match.group(1), match.group(2))) + async with ( run_compiled(yaml_config, line_callback=check_output), api_client_connected() as client, @@ -272,3 +305,111 @@ async def test_text_sensor_raw_state( pytest.fail("Timeout waiting for CHAINED log message") assert state == "[value]", f"Chained failed: expected '[value]', got '{state}'" + + # Test 10: to_lower filter + # "HELLO WORLD" -> "hello world" + to_lower_button = next( + (e for e in entities if "test_to_lower_button" in e.object_id.lower()), + None, + ) + assert to_lower_button is not None, "Test To Lower Button not found" + client.button_command(to_lower_button.key) + + try: + state = await asyncio.wait_for(to_lower_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for TO_LOWER log message") + + assert state == "hello world", ( + f"to_lower failed: expected 'hello world', got '{state}'" + ) + + # Test 11: Lambda filter + # "test" -> "[test]" + lambda_button = next( + (e for e in entities if "test_lambda_button" in e.object_id.lower()), + None, + ) + assert lambda_button is not None, "Test Lambda Button not found" + client.button_command(lambda_button.key) + + try: + state = await asyncio.wait_for(lambda_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for LAMBDA log message") + + assert state == "[test]", f"Lambda failed: expected '[test]', got '{state}'" + + # Test 12: Lambda filter - value passes through + # "value" -> "value passed" + lambda_pass_button = next( + (e for e in entities if "test_lambda_pass_button" in e.object_id.lower()), + None, + ) + assert lambda_pass_button is not None, "Test Lambda Pass Button not found" + client.button_command(lambda_pass_button.key) + + try: + state = await asyncio.wait_for(lambda_pass_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for LAMBDA_PASS log message") + + assert state == "value passed", ( + f"Lambda pass failed: expected 'value passed', got '{state}'" + ) + + # Test 13: Lambda filter - skip publishing (return {}) + # "skip" -> no publish, state remains "value passed" from previous test + lambda_skip_button = next( + (e for e in entities if "test_lambda_skip_button" in e.object_id.lower()), + None, + ) + assert lambda_skip_button is not None, "Test Lambda Skip Button not found" + client.button_command(lambda_skip_button.key) + + try: + state = await asyncio.wait_for(lambda_skip_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for LAMBDA_SKIP log message") + + # When lambda returns {}, value should NOT be published + # State remains from previous successful publish ("value passed") + assert state == "value passed", ( + f"Lambda skip failed: expected 'value passed' (unchanged), got '{state}'" + ) + + # Test 14: Lambda filter - verify raw_state is preserved (not mutated) + # This is critical to verify the in-place mutation optimization is safe + # "original" -> state="original MODIFIED", raw_state="original" + lambda_raw_state_button = next( + ( + e + for e in entities + if "test_lambda_raw_state_button" in e.object_id.lower() + ), + None, + ) + assert lambda_raw_state_button is not None, ( + "Test Lambda Raw State Button not found" + ) + client.button_command(lambda_raw_state_button.key) + + try: + state, raw_state = await asyncio.wait_for( + lambda_raw_state_future, timeout=5.0 + ) + except TimeoutError: + pytest.fail("Timeout waiting for LAMBDA_RAW_STATE log message") + + assert state == "original MODIFIED", ( + f"Lambda raw_state test failed: expected state='original MODIFIED', " + f"got '{state}'" + ) + assert raw_state == "original", ( + f"Lambda raw_state test failed: raw_state was mutated! " + f"Expected 'original', got '{raw_state}'" + ) + assert state != raw_state, ( + f"Lambda filter should modify state but preserve raw_state. " + f"state='{state}', raw_state='{raw_state}'" + ) diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py index 02f965782b..1885b769f1 100644 --- a/tests/unit_tests/test_espota2.py +++ b/tests/unit_tests/test_espota2.py @@ -192,6 +192,20 @@ def test_check_error_unexpected_response() -> None: espota2.check_error([0x7F], [espota2.RESPONSE_OK, espota2.RESPONSE_AUTH_OK]) +def test_check_error_empty_data() -> None: + """Test check_error raises error when device closes connection without responding.""" + with pytest.raises( + espota2.OTAError, match="Device closed connection without responding" + ): + espota2.check_error([], [espota2.RESPONSE_OK]) + + # Also test with empty bytes + with pytest.raises( + espota2.OTAError, match="Device closed connection without responding" + ): + espota2.check_error(b"", [espota2.RESPONSE_OK]) + + def test_send_check_with_various_data_types(mock_socket: Mock) -> None: """Test send_check handles different data types."""