Compare commits

...

22 Commits

Author SHA1 Message Date
J. Nick Koston
37e2f7ccf5 Merge branch 'dev' into libretiny_1100_boards 2026-01-26 17:21:30 -10:00
J. Nick Koston
9c3817f544 [sml] Use constexpr std::array for START_SEQ constant (#13506) 2026-01-26 17:21:17 -10:00
J. Nick Koston
ee9e3315b6 [tm1638] Use member array instead of heap allocation for display buffer (#13504) 2026-01-26 17:21:05 -10:00
J. Nick Koston
67dea1e538 [light] Use member array instead of heap allocation in AddressableLightWrapper (#13503) 2026-01-26 17:20:49 -10:00
J. Nick Koston
003b9c6c3f [uln2003] Refactor step mode logging to use LogString (#13543) 2026-01-26 17:20:33 -10:00
J. Nick Koston
2f1a345905 [mhz19] Refactor detection range logging to use LogString (#13541) 2026-01-26 17:20:21 -10:00
J. Nick Koston
7ef933abec [libretiny] Bump to 1.11.0 (#13512) 2026-01-26 17:20:08 -10:00
J. Nick Koston
4ddd40bcfb [core] Add PROGMEM string comparison helpers and use in cover/valve/helpers (#13545) 2026-01-26 17:19:50 -10:00
J. Nick Koston
8ae901b3f1 [http_request] Use stack allocation for MD5 buffer in OTA (#13550) 2026-01-26 17:19:30 -10:00
J. Nick Koston
bc49174920 Add additional text_sensor filter tests (#13479) 2026-01-26 17:18:36 -10:00
J. Nick Koston
123ee02d39 [ota] Improve error message when device closes connection without responding (#13562) 2026-01-26 17:13:18 -10:00
Jonathan Swoboda
0cc8055757 [http_request] Add custom CA certificate support for ESP32 (#13552)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 22:07:27 -05:00
dependabot[bot]
27a212c14d Bump aioesphomeapi from 43.13.0 to 43.14.0 (#13557)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 15:43:40 -10:00
dependabot[bot]
65dc182526 Bump setuptools from 80.10.1 to 80.10.2 (#13558)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 15:43:27 -10:00
dependabot[bot]
dd91039ff1 Bump github/codeql-action from 4.31.11 to 4.32.0 (#13559)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 15:43:16 -10:00
J. Nick Koston
2f3b2f027d Merge branch 'libretiny_1100' into libretiny_1100_boards 2026-01-25 20:14:22 -10:00
J. Nick Koston
a3ba3a33d9 Merge branch 'dev' into libretiny_1100 2026-01-25 20:14:12 -10:00
sebcaps
1c9a9c7536 [mhz19] Fix Uninitialized var warning message (#13526) 2026-01-25 20:07:29 -10:00
J. Nick Koston
b82149c291 [libretiny] Regenerate boards for v1.11.0 2026-01-25 16:36:38 -10:00
J. Nick Koston
a7223a2cd7 use gh release 2026-01-25 16:23:01 -10:00
J. Nick Koston
3bc1ea45ce bump 2026-01-25 15:22:26 -10:00
J. Nick Koston
3853e3a4dc [libretiny] Bump to 1.10.0 2026-01-24 15:02:03 -10:00
30 changed files with 580 additions and 103 deletions

View File

@@ -1 +1 @@
15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce
d565b0589e35e692b5f2fc0c14723a99595b4828a3a3ef96c442e86a23176c00

View File

@@ -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}}"

View File

@@ -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 <strings.h>
#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);

View File

@@ -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",

View File

@@ -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<HttpContainer> 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_;

View File

@@ -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<HttpContainer> 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);

View File

@@ -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<char[]> 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();

View File

@@ -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",
),
}

View File

@@ -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};
};

View File

@@ -3,8 +3,7 @@
#include <cinttypes>
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,24 +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;
}
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

View File

@@ -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<typename... Ts> class MHZ19DetectionRangeSetAction : public Action<Ts..
void play(const Ts &...x) override { this->parent_->range_set(this->detection_range_.value(x...)); }
};
} // namespace mhz19
} // namespace esphome
} // namespace esphome::mhz19

View File

@@ -23,6 +23,10 @@ RTL87XX_BOARDS = {
"name": "WR2 Wi-Fi Module",
"family": FAMILY_RTL8710B,
},
"wbr3": {
"name": "WBR3 Wi-Fi Module",
"family": FAMILY_RTL8720C,
},
"generic-rtl8710bn-2mb-468k": {
"name": "Generic - RTL8710BN (2M/468k)",
"family": FAMILY_RTL8710B,
@@ -79,6 +83,10 @@ RTL87XX_BOARDS = {
"name": "T103_V1.0",
"family": FAMILY_RTL8710B,
},
"generic-rtl8720cf-2mb-896k": {
"name": "Generic - RTL8720CF (2M/896k)",
"family": FAMILY_RTL8720C,
},
"generic-rtl8720cf-2mb-992k": {
"name": "Generic - RTL8720CF (2M/992k)",
"family": FAMILY_RTL8720C,
@@ -221,6 +229,71 @@ RTL87XX_BOARD_PINS = {
"D9": 29,
"A1": 41,
},
"wbr3": {
"WIRE0_SCL_0": 11,
"WIRE0_SCL_1": 2,
"WIRE0_SCL_2": 19,
"WIRE0_SCL_3": 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": 2,
"SERIAL1_RX_1": 0,
"SERIAL1_TX_0": 3,
"SERIAL1_TX_1": 1,
"SERIAL2_CTS": 19,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CS0": 15,
"CTS1": 4,
"CTS2": 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,
"PA11": 11,
"PA12": 12,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PA19": 19,
"PWM5": 17,
"PWM6": 18,
"RX2": 15,
"SDA0": 16,
"TX2": 16,
"D0": 7,
"D1": 11,
"D2": 2,
"D3": 3,
"D4": 4,
"D5": 12,
"D6": 16,
"D7": 17,
"D8": 18,
"D9": 19,
"D10": 13,
"D11": 14,
"D12": 15,
"D13": 0,
"D14": 1,
},
"generic-rtl8710bn-2mb-468k": {
"SPI0_CS": 19,
"SPI0_MISO": 22,
@@ -1178,6 +1251,104 @@ RTL87XX_BOARD_PINS = {
"A0": 19,
"A1": 41,
},
"generic-rtl8720cf-2mb-896k": {
"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-992k": {
"SPI0_CS_0": 2,
"SPI0_CS_1": 7,

View File

@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <cstdint>
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<uint8_t> START_SEQ = {0x1b, 0x1b, 0x1b, 0x1b, 0x01, 0x01, 0x01, 0x01};
constexpr std::array<uint8_t, 8> START_SEQ = {0x1b, 0x1b, 0x1b, 0x1b, 0x01, 0x01, 0x01, 0x01};
} // namespace sml
} // namespace esphome

View File

@@ -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() {

View File

@@ -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<KeyListener *> listeners_{};
};

View File

@@ -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

View File

@@ -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

View File

@@ -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 <strings.h>
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);

View File

@@ -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 <strings.h>
@@ -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;

View File

@@ -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

View File

@@ -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")

View File

@@ -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 =

View File

@@ -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]

View File

@@ -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

View File

@@ -0,0 +1,7 @@
substitutions:
verify_ssl: "true"
http_request:
ca_certificate_path: $component_dir/test_ca.pem
<<: !include common.yaml

View File

@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBkTCB+wIJAKHBfpegPjMCMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBnVu
dXNlZDAeFw0yNDAxMDEwMDAwMDBaFw0yNTAxMDEwMDAwMDBaMBExDzANBgNVBAMM
BnVudXNlZDBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC5mMUB1hOgLmlnXtsvcGMP
XkhAqZaR0dDPW5OS8VEopWLJCX9Y0cvNCqiDI8cnP8pP8XJGU1hGLvA5PJzWnWZz
AgMBAAGjUzBRMB0GA1UdDgQWBBR5oQ9KqFeZOdBuAJrXxEP0dqzPtTAfBgNVHSME
GDAWgBR5oQ9KqFeZOdBuAJrXxEP0dqzPtTAPBgNVHRMBAf8EBTADAQH/MA0GCSqG
SIb3DQEBCwUAA0EAKqZFf6+f8FPDbKyPCpssquojgn7fEXqr/I/yz0R5CowGdMms
H3WH3aKP4lLSHdPTBtfIoJi3gEIZjFxp3S1TWw==
-----END CERTIFICATE-----

View File

@@ -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};

View File

@@ -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()

View File

@@ -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}'"
)

View File

@@ -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."""