mirror of
https://github.com/esphome/esphome.git
synced 2026-01-25 22:12:09 -07:00
Compare commits
40 Commits
libretiny_
...
2026.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1141e83a7c | ||
|
|
214ce95cf3 | ||
|
|
3a7b83ba93 | ||
|
|
cc2f3d85dc | ||
|
|
723f67d5e2 | ||
|
|
70e45706d9 | ||
|
|
56a2a2269f | ||
|
|
d6841ba33a | ||
|
|
10cbd0164a | ||
|
|
d285706b41 | ||
|
|
ef469c20df | ||
|
|
6870d3dc50 | ||
|
|
9cc39621a6 | ||
|
|
c4f7d09553 | ||
|
|
ab1661ef22 | ||
|
|
ccbf17d5ab | ||
|
|
44e624d7a7 | ||
|
|
f938de16af | ||
|
|
ec791063b3 | ||
|
|
fb984cd052 | ||
|
|
85181779d1 | ||
|
|
95b23702e4 | ||
|
|
95eebcd74f | ||
|
|
3c3d5c2fca | ||
|
|
811ac81320 | ||
|
|
f01bd68a4b | ||
|
|
5433c0f707 | ||
|
|
b06cce9eeb | ||
|
|
65bcfee035 | ||
|
|
9261b9ecaa | ||
|
|
6725e6c01e | ||
|
|
15f0986a59 | ||
|
|
90edf32acf | ||
|
|
055c00f1ac | ||
|
|
7dc40881e2 | ||
|
|
b04373687e | ||
|
|
b89c127f62 | ||
|
|
47dc5d0a1f | ||
|
|
21886dd3ac | ||
|
|
85a5a26519 |
2
Doxyfile
2
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.0b3
|
||||
PROJECT_NUMBER = 2026.1.2
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
import argparse
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
import functools
|
||||
import getpass
|
||||
@@ -931,11 +932,21 @@ def command_dashboard(args: ArgsProtocol) -> int | None:
|
||||
return dashboard.start_dashboard(args)
|
||||
|
||||
|
||||
def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
def run_multiple_configs(
|
||||
files: list, command_builder: Callable[[str], list[str]]
|
||||
) -> int:
|
||||
"""Run a command for each configuration file in a subprocess.
|
||||
|
||||
Args:
|
||||
files: List of configuration files to process.
|
||||
command_builder: Callable that takes a file path and returns a command list.
|
||||
|
||||
Returns:
|
||||
Number of failed files.
|
||||
"""
|
||||
import click
|
||||
|
||||
success = {}
|
||||
files = list_yaml_files(args.configuration)
|
||||
twidth = 60
|
||||
|
||||
def print_bar(middle_text):
|
||||
@@ -945,17 +956,19 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
safe_print(f"{half_line}{middle_text}{half_line}")
|
||||
|
||||
for f in files:
|
||||
safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}")
|
||||
f_path = Path(f) if not isinstance(f, Path) else f
|
||||
|
||||
if any(f_path.name == x for x in SECRETS_FILES):
|
||||
_LOGGER.warning("Skipping secrets file %s", f_path)
|
||||
continue
|
||||
|
||||
safe_print(f"Processing {color(AnsiFore.CYAN, str(f))}")
|
||||
safe_print("-" * twidth)
|
||||
safe_print()
|
||||
if CORE.dashboard:
|
||||
rc = run_external_process(
|
||||
"esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
|
||||
)
|
||||
else:
|
||||
rc = run_external_process(
|
||||
"esphome", "run", f, "--no-logs", "--device", "OTA"
|
||||
)
|
||||
|
||||
cmd = command_builder(f)
|
||||
rc = run_external_process(*cmd)
|
||||
|
||||
if rc == 0:
|
||||
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
|
||||
success[f] = True
|
||||
@@ -970,6 +983,8 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
|
||||
failed = 0
|
||||
for f in files:
|
||||
if f not in success:
|
||||
continue # Skipped file
|
||||
if success[f]:
|
||||
safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}")
|
||||
else:
|
||||
@@ -978,6 +993,17 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
return failed
|
||||
|
||||
|
||||
def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
files = list_yaml_files(args.configuration)
|
||||
|
||||
def build_command(f):
|
||||
if CORE.dashboard:
|
||||
return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"]
|
||||
return ["esphome", "run", f, "--no-logs", "--device", "OTA"]
|
||||
|
||||
return run_multiple_configs(files, build_command)
|
||||
|
||||
|
||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
import json
|
||||
|
||||
@@ -1528,38 +1554,48 @@ def run_esphome(argv):
|
||||
|
||||
_LOGGER.info("ESPHome %s", const.__version__)
|
||||
|
||||
for conf_path in args.configuration:
|
||||
conf_path = Path(conf_path)
|
||||
if any(conf_path.name == x for x in SECRETS_FILES):
|
||||
_LOGGER.warning("Skipping secrets file %s", conf_path)
|
||||
continue
|
||||
# Multiple configurations: use subprocesses to avoid state leakage
|
||||
# between compilations (e.g., LVGL touchscreen state in module globals)
|
||||
if len(args.configuration) > 1:
|
||||
# Build command by reusing argv, replacing all configs with single file
|
||||
# argv[0] is the program path, skip it since we prefix with "esphome"
|
||||
def build_command(f):
|
||||
return (
|
||||
["esphome"]
|
||||
+ [arg for arg in argv[1:] if arg not in args.configuration]
|
||||
+ [str(f)]
|
||||
)
|
||||
|
||||
CORE.config_path = conf_path
|
||||
CORE.dashboard = args.dashboard
|
||||
return run_multiple_configs(args.configuration, build_command)
|
||||
|
||||
# For logs command, skip updating external components
|
||||
skip_external = args.command == "logs"
|
||||
config = read_config(
|
||||
dict(args.substitution) if args.substitution else {},
|
||||
skip_external_update=skip_external,
|
||||
)
|
||||
if config is None:
|
||||
return 2
|
||||
CORE.config = config
|
||||
# Single configuration
|
||||
conf_path = Path(args.configuration[0])
|
||||
if any(conf_path.name == x for x in SECRETS_FILES):
|
||||
_LOGGER.warning("Skipping secrets file %s", conf_path)
|
||||
return 0
|
||||
|
||||
if args.command not in POST_CONFIG_ACTIONS:
|
||||
safe_print(f"Unknown command {args.command}")
|
||||
CORE.config_path = conf_path
|
||||
CORE.dashboard = args.dashboard
|
||||
|
||||
try:
|
||||
rc = POST_CONFIG_ACTIONS[args.command](args, config)
|
||||
except EsphomeError as e:
|
||||
_LOGGER.error(e, exc_info=args.verbose)
|
||||
return 1
|
||||
if rc != 0:
|
||||
return rc
|
||||
# For logs command, skip updating external components
|
||||
skip_external = args.command == "logs"
|
||||
config = read_config(
|
||||
dict(args.substitution) if args.substitution else {},
|
||||
skip_external_update=skip_external,
|
||||
)
|
||||
if config is None:
|
||||
return 2
|
||||
CORE.config = config
|
||||
|
||||
CORE.reset()
|
||||
return 0
|
||||
if args.command not in POST_CONFIG_ACTIONS:
|
||||
safe_print(f"Unknown command {args.command}")
|
||||
return 1
|
||||
|
||||
try:
|
||||
return POST_CONFIG_ACTIONS[args.command](args, config)
|
||||
except EsphomeError as e:
|
||||
_LOGGER.error(e, exc_info=args.verbose)
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -1712,17 +1712,16 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
||||
}
|
||||
|
||||
// Create null-terminated state for callback (parse_number needs null-termination)
|
||||
// HA state max length is 255, so 256 byte buffer covers all cases
|
||||
char state_buf[256];
|
||||
size_t copy_len = msg.state.size();
|
||||
if (copy_len >= sizeof(state_buf)) {
|
||||
copy_len = sizeof(state_buf) - 1; // Truncate to leave space for null terminator
|
||||
// HA state max length is 255 characters, but attributes can be much longer
|
||||
// Use stack buffer for common case (states), heap fallback for large attributes
|
||||
size_t state_len = msg.state.size();
|
||||
SmallBufferWithHeapFallback<256> state_buf_alloc(state_len + 1);
|
||||
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
|
||||
if (state_len > 0) {
|
||||
memcpy(state_buf, msg.state.c_str(), state_len);
|
||||
}
|
||||
if (copy_len > 0) {
|
||||
memcpy(state_buf, msg.state.c_str(), copy_len);
|
||||
}
|
||||
state_buf[copy_len] = '\0';
|
||||
it.callback(StringRef(state_buf, copy_len));
|
||||
state_buf[state_len] = '\0';
|
||||
it.callback(StringRef(state_buf, state_len));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1845,23 +1844,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Toggle Nagle's algorithm based on message type to prevent log messages from
|
||||
// filling the TCP send buffer and crowding out important state updates.
|
||||
//
|
||||
// This honors the `no_delay` proto option - SubscribeLogsResponse is the only
|
||||
// message with `option (no_delay) = false;` in api.proto, indicating it should
|
||||
// allow Nagle coalescing. This option existed since 2019 but was never implemented.
|
||||
//
|
||||
// - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce
|
||||
// into fewer, larger packets. They flush naturally via TCP delayed ACK timer
|
||||
// (~200ms), buffer filling, or when a state update triggers a flush.
|
||||
//
|
||||
// - All other messages (state updates, responses): Disable Nagle (NODELAY=true)
|
||||
// for immediate delivery. These are time-sensitive and should not be delayed.
|
||||
//
|
||||
// This must be done proactively BEFORE the buffer fills up - checking buffer
|
||||
// state here would be too late since we'd already be in a degraded state.
|
||||
this->helper_->set_nodelay(!is_log_message);
|
||||
// Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details
|
||||
this->helper_->set_nodelay_for_message(is_log_message);
|
||||
|
||||
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
|
||||
if (err == APIError::WOULD_BLOCK)
|
||||
|
||||
@@ -120,26 +120,39 @@ class APIFrameHelper {
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
/// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
|
||||
///
|
||||
/// This is used to allow log messages to coalesce (Nagle enabled) while keeping
|
||||
/// state updates low-latency (NODELAY enabled). Without this, many small log
|
||||
/// packets fill the TCP send buffer, crowding out important state updates.
|
||||
///
|
||||
/// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
|
||||
/// this is just a boolean assignment; on other platforms it's a lightweight syscall.
|
||||
///
|
||||
/// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
|
||||
/// @return true if successful or already in desired state
|
||||
bool set_nodelay(bool enable) {
|
||||
if (this->nodelay_enabled_ == enable)
|
||||
return true;
|
||||
int val = enable ? 1 : 0;
|
||||
int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
|
||||
if (err == 0) {
|
||||
this->nodelay_enabled_ = enable;
|
||||
// Manage TCP_NODELAY (Nagle's algorithm) based on message type.
|
||||
//
|
||||
// For non-log messages (sensor data, state updates): Always disable Nagle
|
||||
// (NODELAY on) for immediate delivery - these are time-sensitive.
|
||||
//
|
||||
// For log messages: Use Nagle to coalesce multiple small log packets into
|
||||
// fewer larger packets, reducing WiFi overhead. However, we limit batching
|
||||
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained
|
||||
// devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into
|
||||
// shared pbufs, but holding data too long waiting for Nagle's timer causes
|
||||
// buffer exhaustion and dropped messages.
|
||||
//
|
||||
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all)
|
||||
//
|
||||
void set_nodelay_for_message(bool is_log_message) {
|
||||
if (!is_log_message) {
|
||||
if (this->nodelay_state_ != NODELAY_ON) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd)
|
||||
if (this->nodelay_state_ == NODELAY_ON) {
|
||||
this->set_nodelay_raw_(false);
|
||||
this->nodelay_state_ = 1;
|
||||
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
} else {
|
||||
this->nodelay_state_++;
|
||||
}
|
||||
return err == 0;
|
||||
}
|
||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||
// Write multiple protobuf messages in a single operation
|
||||
@@ -229,10 +242,18 @@ class APIFrameHelper {
|
||||
uint8_t tx_buf_head_{0};
|
||||
uint8_t tx_buf_tail_{0};
|
||||
uint8_t tx_buf_count_{0};
|
||||
// Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true
|
||||
// since init_common_() enables NODELAY. Used by set_nodelay() to allow log
|
||||
// messages to coalesce while keeping state updates low-latency.
|
||||
bool nodelay_enabled_{true};
|
||||
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
|
||||
// (immediate send). Values 1-2 count log messages in the current Nagle batch.
|
||||
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
|
||||
static constexpr int8_t NODELAY_ON = -1;
|
||||
static constexpr int8_t LOG_NAGLE_COUNT = 2;
|
||||
int8_t nodelay_state_{NODELAY_ON};
|
||||
|
||||
// Internal helper to set TCP_NODELAY socket option
|
||||
void set_nodelay_raw_(bool enable) {
|
||||
int val = enable ? 1 : 0;
|
||||
this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
|
||||
}
|
||||
|
||||
// Common initialization for both plaintext and noise protocols
|
||||
APIError init_common_();
|
||||
|
||||
@@ -13,14 +13,11 @@ from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns
|
||||
CODEOWNERS = ["@jasstrong"]
|
||||
DEPENDENCIES = ["sensor"]
|
||||
|
||||
UNIT_INDEX = "index"
|
||||
|
||||
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
AQISensor,
|
||||
unit_of_measurement=UNIT_INDEX,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
|
||||
@@ -89,10 +89,8 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
delayMicroseconds(500);
|
||||
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
|
||||
delayMicroseconds(2000);
|
||||
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
|
||||
delayMicroseconds(1000);
|
||||
} else {
|
||||
delayMicroseconds(800);
|
||||
delayMicroseconds(1000);
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@@ -190,7 +190,7 @@ async def to_code(config):
|
||||
# Rotation is handled by setting the transform
|
||||
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
|
||||
await display.register_display(var, display_config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -180,6 +180,12 @@ def set_core_data(config):
|
||||
path=[CONF_CPU_FREQUENCY],
|
||||
)
|
||||
|
||||
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
|
||||
_LOGGER.warning(
|
||||
"400MHz on ESP32-P4 is experimental and may not boot. "
|
||||
"Consider using 360MHz instead. See https://github.com/esphome/esphome/issues/13425"
|
||||
)
|
||||
|
||||
CORE.data[KEY_ESP32] = {}
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.const import (
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import add_define
|
||||
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
|
||||
@@ -42,6 +43,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
add_define("USE_ESP32_HOSTED")
|
||||
if config[CONF_ACTIVE_HIGH]:
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <esp_ota_ops.h>
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
#include "esphome/components/http_request/http_request.h"
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#endif
|
||||
@@ -181,15 +182,23 @@ bool Esp32HostedUpdate::fetch_manifest_() {
|
||||
}
|
||||
|
||||
// Read manifest JSON into string (manifest is small, ~1KB max)
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
std::string json_str;
|
||||
json_str.reserve(container->content_length);
|
||||
uint8_t buf[256];
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
int read = container->read(buf, sizeof(buf));
|
||||
if (read > 0) {
|
||||
json_str.append(reinterpret_cast<char *>(buf), read);
|
||||
}
|
||||
int read_or_error = container->read(buf, sizeof(buf));
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != http_request::HttpReadLoopResult::DATA)
|
||||
break; // ERROR or TIMEOUT
|
||||
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
|
||||
}
|
||||
container->end();
|
||||
|
||||
@@ -294,32 +303,38 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
|
||||
}
|
||||
|
||||
// Stream firmware to coprocessor while computing SHA256
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
sha256::SHA256 hasher;
|
||||
hasher.init();
|
||||
|
||||
uint8_t buffer[CHUNK_SIZE];
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
|
||||
while (container->get_bytes_read() < total_size) {
|
||||
int read = container->read(buffer, sizeof(buffer));
|
||||
int read_or_error = container->read(buffer, sizeof(buffer));
|
||||
|
||||
// Feed watchdog and give other tasks a chance to run
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
// Exit loop if no data available (stream closed or end of data)
|
||||
if (read <= 0) {
|
||||
if (read < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed with error");
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Download failed"));
|
||||
return false;
|
||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != http_request::HttpReadLoopResult::DATA) {
|
||||
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading firmware data");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading firmware data: %d", read_or_error);
|
||||
}
|
||||
// read == 0: no more data available, exit loop
|
||||
break;
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Download failed"));
|
||||
return false;
|
||||
}
|
||||
|
||||
hasher.add(buffer, read);
|
||||
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT
|
||||
hasher.add(buffer, read_or_error);
|
||||
err = esp_hosted_slave_ota_write(buffer, read_or_error); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "fingerprint_grow.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -532,14 +533,21 @@ void FingerprintGrowComponent::sensor_sleep_() {
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::dump_config() {
|
||||
char sensing_pin_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char power_pin_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
if (this->has_sensing_pin_) {
|
||||
this->sensing_pin_->dump_summary(sensing_pin_buf, sizeof(sensing_pin_buf));
|
||||
}
|
||||
if (this->has_power_pin_) {
|
||||
this->sensor_power_pin_->dump_summary(power_pin_buf, sizeof(power_pin_buf));
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"GROW_FINGERPRINT_READER:\n"
|
||||
" System Identifier Code: 0x%.4X\n"
|
||||
" Touch Sensing Pin: %s\n"
|
||||
" Sensor Power Pin: %s",
|
||||
this->system_identifier_code_,
|
||||
this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None",
|
||||
this->has_power_pin_ ? this->sensor_power_pin_->dump_summary().c_str() : "None");
|
||||
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None",
|
||||
this->has_power_pin_ ? power_pin_buf : "None");
|
||||
if (this->idle_period_to_sleep_ms_ < UINT32_MAX) {
|
||||
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
|
||||
} else {
|
||||
|
||||
@@ -157,6 +157,7 @@ async def to_code(config):
|
||||
if CORE.is_esp32:
|
||||
cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX]))
|
||||
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
|
||||
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)
|
||||
|
||||
@@ -79,6 +79,81 @@ inline bool is_redirect(int const status) {
|
||||
*/
|
||||
inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; }
|
||||
|
||||
/*
|
||||
* HTTP Container Read Semantics
|
||||
* =============================
|
||||
*
|
||||
* IMPORTANT: These semantics differ from standard BSD sockets!
|
||||
*
|
||||
* BSD socket read() returns:
|
||||
* > 0: bytes read
|
||||
* == 0: connection closed (EOF)
|
||||
* < 0: error (check errno)
|
||||
*
|
||||
* HttpContainer::read() returns:
|
||||
* > 0: bytes read successfully
|
||||
* == 0: no data available yet OR all content read
|
||||
* (caller should check bytes_read vs content_length)
|
||||
* < 0: error or connection closed (caller should EXIT)
|
||||
* HTTP_ERROR_CONNECTION_CLOSED (-1) = connection closed prematurely
|
||||
* other negative values = platform-specific errors
|
||||
*
|
||||
* Platform behaviors:
|
||||
* - ESP-IDF: blocking reads, 0 only returned when all content read
|
||||
* - Arduino: non-blocking, 0 means "no data yet" or "all content read"
|
||||
*
|
||||
* Use the helper functions below instead of checking return values directly:
|
||||
* - http_read_loop_result(): for manual loops with per-chunk processing
|
||||
* - http_read_fully(): for simple "read N bytes into buffer" operations
|
||||
*/
|
||||
|
||||
/// Error code returned by HttpContainer::read() when connection closed prematurely
|
||||
/// NOTE: Unlike BSD sockets where 0 means EOF, here 0 means "no data yet, retry"
|
||||
static constexpr int HTTP_ERROR_CONNECTION_CLOSED = -1;
|
||||
|
||||
/// Status of a read operation
|
||||
enum class HttpReadStatus : uint8_t {
|
||||
OK, ///< Read completed successfully
|
||||
ERROR, ///< Read error occurred
|
||||
TIMEOUT, ///< Timeout waiting for data
|
||||
};
|
||||
|
||||
/// Result of an HTTP read operation
|
||||
struct HttpReadResult {
|
||||
HttpReadStatus status; ///< Status of the read operation
|
||||
int error_code; ///< Error code from read() on failure, 0 on success
|
||||
};
|
||||
|
||||
/// Result of processing a non-blocking read with timeout (for manual loops)
|
||||
enum class HttpReadLoopResult : uint8_t {
|
||||
DATA, ///< Data was read, process it
|
||||
RETRY, ///< No data yet, already delayed, caller should continue loop
|
||||
ERROR, ///< Read error, caller should exit loop
|
||||
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
|
||||
};
|
||||
|
||||
/// Process a read result with timeout tracking and delay handling
|
||||
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
|
||||
/// @param last_data_time Time of last successful read, updated when data received
|
||||
/// @param timeout_ms Maximum time to wait for data
|
||||
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
|
||||
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
|
||||
uint32_t timeout_ms) {
|
||||
if (bytes_read_or_error > 0) {
|
||||
last_data_time = millis();
|
||||
return HttpReadLoopResult::DATA;
|
||||
}
|
||||
if (bytes_read_or_error < 0) {
|
||||
return HttpReadLoopResult::ERROR;
|
||||
}
|
||||
// bytes_read_or_error == 0: no data available yet
|
||||
if (millis() - last_data_time >= timeout_ms) {
|
||||
return HttpReadLoopResult::TIMEOUT;
|
||||
}
|
||||
delay(1); // Small delay to prevent tight spinning
|
||||
return HttpReadLoopResult::RETRY;
|
||||
}
|
||||
|
||||
class HttpRequestComponent;
|
||||
|
||||
class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
@@ -88,6 +163,33 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
int status_code;
|
||||
uint32_t duration_ms;
|
||||
|
||||
/**
|
||||
* @brief Read data from the HTTP response body.
|
||||
*
|
||||
* WARNING: These semantics differ from BSD sockets!
|
||||
* BSD sockets: 0 = EOF (connection closed)
|
||||
* This method: 0 = no data yet OR all content read, negative = error/closed
|
||||
*
|
||||
* @param buf Buffer to read data into
|
||||
* @param max_len Maximum number of bytes to read
|
||||
* @return
|
||||
* - > 0: Number of bytes read successfully
|
||||
* - 0: No data available yet OR all content read
|
||||
* (check get_bytes_read() >= content_length to distinguish)
|
||||
* - HTTP_ERROR_CONNECTION_CLOSED (-1): Connection closed prematurely
|
||||
* - < -1: Other error (platform-specific error code)
|
||||
*
|
||||
* Platform notes:
|
||||
* - ESP-IDF: blocking read, 0 only when all content read
|
||||
* - Arduino: non-blocking, 0 can mean "no data yet" or "all content read"
|
||||
*
|
||||
* Use get_bytes_read() and content_length to track progress.
|
||||
* When get_bytes_read() >= content_length, all data has been received.
|
||||
*
|
||||
* IMPORTANT: Do not use raw return values directly. Use these helpers:
|
||||
* - http_read_loop_result(): for loops with per-chunk processing
|
||||
* - http_read_fully(): for simple "read N bytes" operations
|
||||
*/
|
||||
virtual int read(uint8_t *buf, size_t max_len) = 0;
|
||||
virtual void end() = 0;
|
||||
|
||||
@@ -110,6 +212,38 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
std::map<std::string, std::list<std::string>> response_headers_{};
|
||||
};
|
||||
|
||||
/// Read data from HTTP container into buffer with timeout handling
|
||||
/// Handles feed_wdt, yield, and timeout checking internally
|
||||
/// @param container The HTTP container to read from
|
||||
/// @param buffer Buffer to read into
|
||||
/// @param total_size Total bytes to read
|
||||
/// @param chunk_size Maximum bytes per read call
|
||||
/// @param timeout_ms Read timeout in milliseconds
|
||||
/// @return HttpReadResult with status and error_code on failure
|
||||
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
|
||||
uint32_t timeout_ms) {
|
||||
size_t read_index = 0;
|
||||
uint32_t last_data_time = millis();
|
||||
|
||||
while (read_index < total_size) {
|
||||
int read_bytes_or_error = container->read(buffer + read_index, std::min(chunk_size, total_size - read_index));
|
||||
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result == HttpReadLoopResult::ERROR)
|
||||
return {HttpReadStatus::ERROR, read_bytes_or_error};
|
||||
if (result == HttpReadLoopResult::TIMEOUT)
|
||||
return {HttpReadStatus::TIMEOUT, 0};
|
||||
|
||||
read_index += read_bytes_or_error;
|
||||
}
|
||||
return {HttpReadStatus::OK, 0};
|
||||
}
|
||||
|
||||
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> {
|
||||
public:
|
||||
void process(const std::shared_ptr<HttpContainer> &container, std::string &response_body) {
|
||||
@@ -124,6 +258,7 @@ class HttpRequestComponent : public Component {
|
||||
|
||||
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
|
||||
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
|
||||
uint32_t get_timeout() const { return this->timeout_; }
|
||||
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
|
||||
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
|
||||
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
|
||||
@@ -249,15 +384,21 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *buf = allocator.allocate(max_length);
|
||||
if (buf != nullptr) {
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see top of this file
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
size_t read_index = 0;
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->parent_->get_timeout();
|
||||
while (container->get_bytes_read() < max_length) {
|
||||
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
if (read <= 0) {
|
||||
break;
|
||||
}
|
||||
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
read_index += read;
|
||||
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != HttpReadLoopResult::DATA)
|
||||
break; // ERROR or TIMEOUT
|
||||
read_index += read_or_error;
|
||||
}
|
||||
response_body.reserve(read_index);
|
||||
response_body.assign((char *) buf, read_index);
|
||||
|
||||
@@ -139,6 +139,23 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
return container;
|
||||
}
|
||||
|
||||
// Arduino HTTP read implementation
|
||||
//
|
||||
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
||||
//
|
||||
// Arduino's WiFiClient is inherently non-blocking - available() returns 0 when
|
||||
// no data is ready. We use connected() to distinguish "no data yet" from
|
||||
// "connection closed".
|
||||
//
|
||||
// WiFiClient behavior:
|
||||
// available() > 0: data ready to read
|
||||
// available() == 0 && connected(): no data yet, still connected
|
||||
// available() == 0 && !connected(): connection closed
|
||||
//
|
||||
// We normalize to HttpContainer::read() contract (NOT BSD socket semantics!):
|
||||
// > 0: bytes read
|
||||
// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF!
|
||||
// < 0: error/connection closed <-- connection closed returns -1, not 0
|
||||
int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
@@ -146,7 +163,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
WiFiClient *stream_ptr = this->client_.getStreamPtr();
|
||||
if (stream_ptr == nullptr) {
|
||||
ESP_LOGE(TAG, "Stream pointer vanished!");
|
||||
return -1;
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
|
||||
int available_data = stream_ptr->available();
|
||||
@@ -154,7 +171,15 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
|
||||
if (bufsize == 0) {
|
||||
this->duration_ms += (millis() - start);
|
||||
return 0;
|
||||
// Check if we've read all expected content
|
||||
if (this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
// No data available - check if connection is still open
|
||||
if (!stream_ptr->connected()) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed prematurely
|
||||
}
|
||||
return 0; // No data yet, caller should retry
|
||||
}
|
||||
|
||||
App.feed_wdt();
|
||||
|
||||
@@ -89,7 +89,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
config.max_redirection_count = this->redirect_limit_;
|
||||
config.auth_type = HTTP_AUTH_TYPE_BASIC;
|
||||
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
|
||||
if (secure) {
|
||||
if (secure && this->verify_ssl_) {
|
||||
config.crt_bundle_attach = esp_crt_bundle_attach;
|
||||
}
|
||||
#endif
|
||||
@@ -209,26 +209,57 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
return container;
|
||||
}
|
||||
|
||||
// ESP-IDF HTTP read implementation (blocking mode)
|
||||
//
|
||||
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
||||
//
|
||||
// esp_http_client_read() in blocking mode returns:
|
||||
// > 0: bytes read
|
||||
// 0: connection closed (end of stream)
|
||||
// < 0: error
|
||||
//
|
||||
// We normalize to HttpContainer::read() contract:
|
||||
// > 0: bytes read
|
||||
// 0: no data yet / all content read (caller should check bytes_read vs content_length)
|
||||
// < 0: error/connection closed
|
||||
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
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;
|
||||
// Check if we've already read all expected content
|
||||
if (this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
|
||||
this->feed_wdt();
|
||||
int read_len_or_error = esp_http_client_read(this->client_, (char *) buf, max_len);
|
||||
this->feed_wdt();
|
||||
|
||||
this->duration_ms += (millis() - start);
|
||||
|
||||
return read_len;
|
||||
if (read_len_or_error > 0) {
|
||||
this->bytes_read_ += read_len_or_error;
|
||||
return read_len_or_error;
|
||||
}
|
||||
|
||||
// Connection closed by server before all content received
|
||||
if (read_len_or_error == 0) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
|
||||
// Negative value - error, return the actual error code for debugging
|
||||
return read_len_or_error;
|
||||
}
|
||||
|
||||
void HttpContainerIDF::end() {
|
||||
if (this->client_ == nullptr) {
|
||||
return; // Already cleaned up
|
||||
}
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
esp_http_client_close(this->client_);
|
||||
esp_http_client_cleanup(this->client_);
|
||||
this->client_ = nullptr;
|
||||
}
|
||||
|
||||
void HttpContainerIDF::feed_wdt() {
|
||||
|
||||
@@ -34,6 +34,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; }
|
||||
|
||||
protected:
|
||||
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
||||
@@ -42,6 +43,7 @@ class HttpRequestIDF : public HttpRequestComponent {
|
||||
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
|
||||
uint16_t buffer_size_rx_{};
|
||||
uint16_t buffer_size_tx_{};
|
||||
bool verify_ssl_{true};
|
||||
|
||||
/// @brief Monitors the http client events to gather response headers
|
||||
static esp_err_t http_event_handler(esp_http_client_event_t *evt);
|
||||
|
||||
@@ -115,39 +115,47 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
return error_code;
|
||||
}
|
||||
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->parent_->get_timeout();
|
||||
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
// read a maximum of chunk_size bytes into buf. (real read size returned)
|
||||
int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
|
||||
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(),
|
||||
container->content_length, bufsize);
|
||||
// read a maximum of chunk_size bytes into buf. (real read size returned, or negative error code)
|
||||
int bufsize_or_error = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
|
||||
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize_or_error = %i", container->get_bytes_read(),
|
||||
container->content_length, bufsize_or_error);
|
||||
|
||||
// feed watchdog and give other tasks a chance to run
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
// Exit loop if no data available (stream closed or end of data)
|
||||
if (bufsize <= 0) {
|
||||
if (bufsize < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed with error");
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != HttpReadLoopResult::DATA) {
|
||||
if (result == HttpReadLoopResult::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading data");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
|
||||
}
|
||||
// bufsize == 0: no more data available, exit loop
|
||||
break;
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
}
|
||||
|
||||
if (bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
|
||||
// At this point bufsize_or_error > 0, so it's a valid size
|
||||
if (bufsize_or_error <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
|
||||
// add read bytes to MD5
|
||||
md5_receive.add(buf, bufsize);
|
||||
md5_receive.add(buf, bufsize_or_error);
|
||||
|
||||
// write bytes to OTA backend
|
||||
this->update_started_ = true;
|
||||
error_code = backend->write(buf, bufsize);
|
||||
error_code = backend->write(buf, bufsize_or_error);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
// error code explanation available at
|
||||
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
|
||||
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
|
||||
container->get_bytes_read() - bufsize, container->content_length);
|
||||
container->get_bytes_read() - bufsize_or_error, container->content_length);
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return error_code;
|
||||
}
|
||||
@@ -244,19 +252,19 @@ bool OtaHttpRequestComponent::http_get_md5_() {
|
||||
}
|
||||
|
||||
this->md5_expected_.resize(MD5_SIZE);
|
||||
int read_len = 0;
|
||||
while (container->get_bytes_read() < MD5_SIZE) {
|
||||
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
|
||||
if (read_len <= 0) {
|
||||
break;
|
||||
}
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
}
|
||||
auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
|
||||
this->parent_->get_timeout());
|
||||
container->end();
|
||||
|
||||
ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE);
|
||||
return read_len == MD5_SIZE;
|
||||
if (result.status != HttpReadStatus::OK) {
|
||||
if (result.status == HttpReadStatus::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading MD5");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading MD5: %d", result.error_code);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaHttpRequestComponent::validate_url_(const std::string &url) {
|
||||
|
||||
@@ -11,7 +11,12 @@ namespace http_request {
|
||||
|
||||
// The update function runs in a task only on ESP32s.
|
||||
#ifdef USE_ESP32
|
||||
#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task
|
||||
// vTaskDelete doesn't return, but clang-tidy doesn't know that
|
||||
#define UPDATE_RETURN \
|
||||
do { \
|
||||
vTaskDelete(nullptr); \
|
||||
__builtin_unreachable(); \
|
||||
} while (0)
|
||||
#else
|
||||
#define UPDATE_RETURN return
|
||||
#endif
|
||||
@@ -70,19 +75,21 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
|
||||
size_t read_index = 0;
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
int read_bytes = container->read(data + read_index, MAX_READ_SIZE);
|
||||
|
||||
yield();
|
||||
|
||||
if (read_bytes <= 0) {
|
||||
// Network error or connection closed - break to avoid infinite loop
|
||||
break;
|
||||
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
|
||||
this_update->request_parent_->get_timeout());
|
||||
if (read_result.status != HttpReadStatus::OK) {
|
||||
if (read_result.status == HttpReadStatus::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading manifest");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
|
||||
}
|
||||
|
||||
read_index += read_bytes;
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); });
|
||||
allocator.deallocate(data, container->content_length);
|
||||
container->end();
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
size_t read_index = container->get_bytes_read();
|
||||
|
||||
bool valid = false;
|
||||
{ // Ensures the response string falls out of scope and deallocates before the task ends
|
||||
|
||||
@@ -42,8 +42,8 @@ ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t
|
||||
}
|
||||
|
||||
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const {
|
||||
SmallBufferWithHeapFallback<17> buffer_alloc; // Most I2C writes are <= 16 bytes
|
||||
uint8_t *buffer = buffer_alloc.get(len + 1);
|
||||
SmallBufferWithHeapFallback<17> buffer_alloc(len + 1); // Most I2C writes are <= 16 bytes
|
||||
uint8_t *buffer = buffer_alloc.get();
|
||||
|
||||
buffer[0] = a_register;
|
||||
std::copy(data, data + len, buffer + 1);
|
||||
@@ -51,8 +51,8 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz
|
||||
}
|
||||
|
||||
ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const {
|
||||
SmallBufferWithHeapFallback<18> buffer_alloc; // Most I2C writes are <= 16 bytes + 2 for register
|
||||
uint8_t *buffer = buffer_alloc.get(len + 2);
|
||||
SmallBufferWithHeapFallback<18> buffer_alloc(len + 2); // Most I2C writes are <= 16 bytes + 2 for register
|
||||
uint8_t *buffer = buffer_alloc.get();
|
||||
|
||||
buffer[0] = a_register >> 8;
|
||||
buffer[1] = a_register;
|
||||
|
||||
@@ -11,22 +11,6 @@
|
||||
namespace esphome {
|
||||
namespace i2c {
|
||||
|
||||
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
|
||||
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
|
||||
public:
|
||||
uint8_t *get(size_t size) {
|
||||
if (size <= STACK_SIZE) {
|
||||
return this->stack_buffer_;
|
||||
}
|
||||
this->heap_buffer_ = std::unique_ptr<uint8_t[]>(new uint8_t[size]);
|
||||
return this->heap_buffer_.get();
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t stack_buffer_[STACK_SIZE];
|
||||
std::unique_ptr<uint8_t[]> heap_buffer_;
|
||||
};
|
||||
|
||||
/// @brief Error codes returned by I2CBus and I2CDevice methods
|
||||
enum ErrorCode {
|
||||
NO_ERROR = 0, ///< No error found during execution of method
|
||||
@@ -92,8 +76,8 @@ class I2CBus {
|
||||
total_len += read_buffers[i].len;
|
||||
}
|
||||
|
||||
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C reads are small
|
||||
uint8_t *buffer = buffer_alloc.get(total_len);
|
||||
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small
|
||||
uint8_t *buffer = buffer_alloc.get();
|
||||
|
||||
auto err = this->write_readv(address, nullptr, 0, buffer, total_len);
|
||||
if (err != ERROR_OK)
|
||||
@@ -116,8 +100,8 @@ class I2CBus {
|
||||
total_len += write_buffers[i].len;
|
||||
}
|
||||
|
||||
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C writes are small
|
||||
uint8_t *buffer = buffer_alloc.get(total_len);
|
||||
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small
|
||||
uint8_t *buffer = buffer_alloc.get();
|
||||
|
||||
size_t pos = 0;
|
||||
for (size_t i = 0; i != count; i++) {
|
||||
|
||||
@@ -185,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
|
||||
}
|
||||
jobs[num_jobs++].command = I2C_MASTER_CMD_STOP;
|
||||
ESP_LOGV(TAG, "Sending %zu jobs", num_jobs);
|
||||
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 20);
|
||||
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100);
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGV(TAG, "TX to %02X failed: not acked", address);
|
||||
return ERROR_NOT_ACKNOWLEDGED;
|
||||
|
||||
@@ -223,7 +223,7 @@ async def to_code(config):
|
||||
var = cg.Pvariable(config[CONF_ID], rhs)
|
||||
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
if init_sequences := config.get(CONF_INIT_SEQUENCE):
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Once the API is considered stable, this warning will be removed.
|
||||
|
||||
#include "esphome/components/infrared/infrared.h"
|
||||
#include "esphome/components/remote_transmitter/remote_transmitter.h"
|
||||
#include "esphome/components/remote_receiver/remote_receiver.h"
|
||||
|
||||
namespace esphome::ir_rf_proxy {
|
||||
|
||||
|
||||
@@ -391,7 +391,10 @@ void LightCall::transform_parameters_() {
|
||||
min_mireds > 0.0f && max_mireds > 0.0f) {
|
||||
ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
|
||||
this->parent_->get_name().c_str());
|
||||
if (this->has_color_temperature()) {
|
||||
// Only compute cold_white/warm_white from color_temperature if they're not already explicitly set.
|
||||
// This is important for state restoration, where both color_temperature and cold_white/warm_white
|
||||
// are restored from flash - we want to preserve the saved cold_white/warm_white values.
|
||||
if (this->has_color_temperature() && !this->has_cold_white() && !this->has_warm_white()) {
|
||||
const float color_temp = clamp(this->color_temperature_, min_mireds, max_mireds);
|
||||
const float range = max_mireds - min_mireds;
|
||||
const float ww_fraction = (color_temp - min_mireds) / range;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from esphome import codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_OPTIONS
|
||||
|
||||
@@ -24,6 +25,34 @@ from .label import CONF_LABEL
|
||||
CONF_DROPDOWN = "dropdown"
|
||||
CONF_DROPDOWN_LIST = "dropdown_list"
|
||||
|
||||
# Example valid dropdown symbol (left arrow) for error messages
|
||||
EXAMPLE_DROPDOWN_SYMBOL = "\U00002190" # ←
|
||||
|
||||
|
||||
def dropdown_symbol_validator(value):
|
||||
"""
|
||||
Validate that the dropdown symbol is a single Unicode character
|
||||
with a codepoint of 0x100 (256) or greater.
|
||||
This is required because LVGL uses codepoints below 0x100 for internal symbols.
|
||||
"""
|
||||
value = cv.string(value)
|
||||
# len(value) counts Unicode code points, not grapheme clusters or bytes
|
||||
if len(value) != 1:
|
||||
raise cv.Invalid(
|
||||
f"Dropdown symbol must be a single character, got '{value}' with length {len(value)}"
|
||||
)
|
||||
codepoint = ord(value)
|
||||
if codepoint < 0x100:
|
||||
# Format the example symbol as a Unicode escape for the error message
|
||||
example_escape = f"\\U{ord(EXAMPLE_DROPDOWN_SYMBOL):08X}"
|
||||
raise cv.Invalid(
|
||||
f"Dropdown symbol must have a Unicode codepoint of 0x100 (256) or greater. "
|
||||
f"'{value}' has codepoint {codepoint} (0x{codepoint:X}). "
|
||||
f"Use a character like '{example_escape}' ({EXAMPLE_DROPDOWN_SYMBOL}) or other Unicode symbols with codepoint >= 0x100."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
lv_dropdown_t = LvSelect("LvDropdownType", parents=(LvCompound,))
|
||||
|
||||
lv_dropdown_list_t = LvType("lv_dropdown_list_t")
|
||||
@@ -33,7 +62,7 @@ dropdown_list_spec = WidgetType(
|
||||
|
||||
DROPDOWN_BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SYMBOL): lv_text,
|
||||
cv.Optional(CONF_SYMBOL): dropdown_symbol_validator,
|
||||
cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int,
|
||||
cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text,
|
||||
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts),
|
||||
@@ -70,7 +99,7 @@ class DropdownType(WidgetType):
|
||||
if options := config.get(CONF_OPTIONS):
|
||||
lv_add(w.var.set_options(options))
|
||||
if symbol := config.get(CONF_SYMBOL):
|
||||
lv.dropdown_set_symbol(w.var.obj, await lv_text.process(symbol))
|
||||
lv.dropdown_set_symbol(w.var.obj, cg.safe_exp(symbol))
|
||||
if (selected := config.get(CONF_SELECTED_INDEX)) is not None:
|
||||
value = await lv_int.process(selected)
|
||||
lv_add(w.var.set_selected_index(value, literal("LV_ANIM_OFF")))
|
||||
|
||||
@@ -32,7 +32,7 @@ class LabelType(WidgetType):
|
||||
|
||||
async def to_code(self, w: Widget, config):
|
||||
"""For a text object, create and set text"""
|
||||
if value := config.get(CONF_TEXT):
|
||||
if (value := config.get(CONF_TEXT)) is not None:
|
||||
await w.set_property(CONF_TEXT, await lv_text.process(value))
|
||||
await w.set_property(CONF_LONG_MODE, config)
|
||||
await w.set_property(CONF_RECOLOR, config)
|
||||
|
||||
@@ -29,7 +29,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await display.register_display(var, config)
|
||||
|
||||
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
|
||||
|
||||
@@ -86,7 +86,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await display.register_display(var, config)
|
||||
|
||||
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
|
||||
|
||||
@@ -260,7 +260,7 @@ async def to_code(config):
|
||||
cg.add(var.set_enable_pins(enable))
|
||||
|
||||
if CONF_SPI_ID in config:
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
sequence, madctl = model.get_sequence(config)
|
||||
cg.add(var.set_init_sequence(sequence))
|
||||
cg.add(var.set_madctl(madctl))
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#include "mipi_rgb.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esp_lcd_panel_rgb.h"
|
||||
#include <span>
|
||||
|
||||
namespace esphome {
|
||||
namespace mipi_rgb {
|
||||
@@ -343,19 +345,27 @@ int MipiRgb::get_height() {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string get_pin_name(GPIOPin *pin) {
|
||||
static const char *get_pin_name(GPIOPin *pin, std::span<char, GPIO_SUMMARY_MAX_LEN> buffer) {
|
||||
if (pin == nullptr)
|
||||
return "None";
|
||||
return pin->dump_summary();
|
||||
pin->dump_summary(buffer.data(), buffer.size());
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
void MipiRgb::dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset) {
|
||||
char pin_summary[GPIO_SUMMARY_MAX_LEN];
|
||||
for (uint8_t i = start; i != end; i++) {
|
||||
ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, this->data_pins_[i]->dump_summary().c_str());
|
||||
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, pin_summary);
|
||||
}
|
||||
}
|
||||
|
||||
void MipiRgb::dump_config() {
|
||||
char reset_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char de_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char pclk_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char hsync_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char vsync_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"MIPI_RGB LCD"
|
||||
"\n Model: %s"
|
||||
@@ -379,9 +389,9 @@ void MipiRgb::dump_config() {
|
||||
this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_),
|
||||
this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_,
|
||||
this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_),
|
||||
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(),
|
||||
get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(),
|
||||
get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str());
|
||||
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_, reset_buf),
|
||||
get_pin_name(this->de_pin_, de_buf), get_pin_name(this->pclk_pin_, pclk_buf),
|
||||
get_pin_name(this->hsync_pin_, hsync_buf), get_pin_name(this->vsync_pin_, vsync_buf));
|
||||
|
||||
this->dump_pins_(8, 13, "Blue", 0);
|
||||
this->dump_pins_(13, 16, "Green", 0);
|
||||
|
||||
@@ -55,6 +55,7 @@ st7701s = ST7701S(
|
||||
pclk_frequency="16MHz",
|
||||
pclk_inverted=True,
|
||||
initsequence=(
|
||||
(0x01,), # Software Reset
|
||||
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0
|
||||
(0xC0, 0x3B, 0x00), (0xC1, 0x0D, 0x02), (0xC2, 0x31, 0x05),
|
||||
(0xB0, 0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18,),
|
||||
|
||||
@@ -443,6 +443,4 @@ async def to_code(config):
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
# Displays are write-only, set the SPI device to write-only as well
|
||||
cg.add(var.set_write_only(True))
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
@@ -279,7 +279,7 @@ def modbus_calc_properties(config):
|
||||
if isinstance(value, str):
|
||||
value = value.encode()
|
||||
config[CONF_ADDRESS] = binascii.crc_hqx(value, 0)
|
||||
config[CONF_REGISTER_TYPE] = ModbusRegisterType.CUSTOM
|
||||
config[CONF_REGISTER_TYPE] = cv.enum(MODBUS_REGISTER_TYPE)("custom")
|
||||
config[CONF_FORCE_NEW_RANGE] = True
|
||||
return byte_offset, reg_count
|
||||
|
||||
|
||||
@@ -43,6 +43,14 @@ namespace network {
|
||||
/// Buffer size for IP address string (IPv6 max: 39 chars + null)
|
||||
static constexpr size_t IP_ADDRESS_BUFFER_SIZE = 40;
|
||||
|
||||
/// Lowercase hex digits in IP address string (A-F -> a-f for IPv6 per RFC 5952)
|
||||
inline void lowercase_ip_str(char *buf) {
|
||||
for (char *p = buf; *p; ++p) {
|
||||
if (*p >= 'A' && *p <= 'F')
|
||||
*p += 32;
|
||||
}
|
||||
}
|
||||
|
||||
struct IPAddress {
|
||||
public:
|
||||
#ifdef USE_HOST
|
||||
@@ -52,10 +60,15 @@ struct IPAddress {
|
||||
}
|
||||
IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); }
|
||||
IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; }
|
||||
std::string str() const { return str_lower_case(inet_ntoa(ip_addr_)); }
|
||||
std::string str() const {
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
this->str_to(buf);
|
||||
return buf;
|
||||
}
|
||||
/// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes.
|
||||
char *str_to(char *buf) const {
|
||||
return const_cast<char *>(inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE));
|
||||
inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE);
|
||||
return buf; // IPv4 only, no hex letters to lowercase
|
||||
}
|
||||
#else
|
||||
IPAddress() { ip_addr_set_zero(&ip_addr_); }
|
||||
@@ -134,9 +147,18 @@ struct IPAddress {
|
||||
bool is_ip4() const { return IP_IS_V4(&ip_addr_); }
|
||||
bool is_ip6() const { return IP_IS_V6(&ip_addr_); }
|
||||
bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); }
|
||||
std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); }
|
||||
std::string str() const {
|
||||
char buf[IP_ADDRESS_BUFFER_SIZE];
|
||||
this->str_to(buf);
|
||||
return buf;
|
||||
}
|
||||
/// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes.
|
||||
char *str_to(char *buf) const { return ipaddr_ntoa_r(&ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE); }
|
||||
/// Output is lowercased per RFC 5952 (IPv6 hex digits a-f).
|
||||
char *str_to(char *buf) const {
|
||||
ipaddr_ntoa_r(&ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE);
|
||||
lowercase_ip_str(buf);
|
||||
return buf;
|
||||
}
|
||||
bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
|
||||
bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
|
||||
IPAddress &operator+=(uint8_t increase) {
|
||||
|
||||
@@ -44,7 +44,7 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -161,7 +161,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
chip = DriverChip.chips[config[CONF_MODEL]]
|
||||
if chip.initsequence:
|
||||
|
||||
@@ -133,14 +133,17 @@ void RD03DComponent::process_frame_() {
|
||||
uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE);
|
||||
|
||||
// Extract raw bytes for this target
|
||||
// Note: Despite datasheet Table 5-2 showing order as X, Y, Speed, Resolution,
|
||||
// actual radar output has Resolution before Speed (verified empirically -
|
||||
// stationary targets were showing non-zero speed with original field order)
|
||||
uint8_t x_low = this->buffer_[offset + 0];
|
||||
uint8_t x_high = this->buffer_[offset + 1];
|
||||
uint8_t y_low = this->buffer_[offset + 2];
|
||||
uint8_t y_high = this->buffer_[offset + 3];
|
||||
uint8_t speed_low = this->buffer_[offset + 4];
|
||||
uint8_t speed_high = this->buffer_[offset + 5];
|
||||
uint8_t res_low = this->buffer_[offset + 6];
|
||||
uint8_t res_high = this->buffer_[offset + 7];
|
||||
uint8_t res_low = this->buffer_[offset + 4];
|
||||
uint8_t res_high = this->buffer_[offset + 5];
|
||||
uint8_t speed_low = this->buffer_[offset + 6];
|
||||
uint8_t speed_high = this->buffer_[offset + 7];
|
||||
|
||||
// Decode values per RD-03D format
|
||||
int16_t x = decode_value(x_low, x_high);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#include "rpi_dpi_rgb.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -134,8 +135,11 @@ void RpiDpiRgb::dump_config() {
|
||||
LOG_PIN(" Enable Pin: ", this->enable_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
|
||||
for (size_t i = 0; i != data_pin_count; i++)
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str());
|
||||
char pin_summary[GPIO_SUMMARY_MAX_LEN];
|
||||
for (size_t i = 0; i != data_pin_count; i++) {
|
||||
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
|
||||
}
|
||||
}
|
||||
|
||||
void RpiDpiRgb::reset_display_() const {
|
||||
|
||||
@@ -124,8 +124,8 @@ void SEN5XComponent::setup() {
|
||||
sen5x_type = SEN55;
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str());
|
||||
}
|
||||
ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str());
|
||||
if (this->humidity_sensor_ && sen5x_type == SEN50) {
|
||||
ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55");
|
||||
this->humidity_sensor_ = nullptr; // mark as not used
|
||||
@@ -159,28 +159,14 @@ void SEN5XComponent::setup() {
|
||||
// This ensures the baseline storage is cleared after OTA
|
||||
// Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
|
||||
uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), combined_serial);
|
||||
this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true);
|
||||
|
||||
if (this->pref_.load(&this->voc_baselines_storage_)) {
|
||||
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
|
||||
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
|
||||
}
|
||||
|
||||
// Initialize storage timestamp
|
||||
this->seconds_since_last_store_ = 0;
|
||||
|
||||
if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
|
||||
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
|
||||
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
|
||||
uint16_t states[4];
|
||||
|
||||
states[0] = this->voc_baselines_storage_.state0 >> 16;
|
||||
states[1] = this->voc_baselines_storage_.state0 & 0xFFFF;
|
||||
states[2] = this->voc_baselines_storage_.state1 >> 16;
|
||||
states[3] = this->voc_baselines_storage_.state1 & 0xFFFF;
|
||||
|
||||
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) {
|
||||
ESP_LOGE(TAG, "Failed to set VOC baseline from saved state");
|
||||
this->pref_ = global_preferences->make_preference<uint16_t[4]>(hash, true);
|
||||
this->voc_baseline_time_ = App.get_loop_component_start_time();
|
||||
if (this->pref_.load(&this->voc_baseline_state_)) {
|
||||
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, this->voc_baseline_state_, 4)) {
|
||||
ESP_LOGE(TAG, "VOC Baseline State write to sensor failed");
|
||||
} else {
|
||||
ESP_LOGV(TAG, "VOC Baseline State loaded");
|
||||
delay(20);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,6 +274,14 @@ void SEN5XComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s",
|
||||
LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value())));
|
||||
}
|
||||
if (this->voc_sensor_) {
|
||||
char hex_buf[5 * 4];
|
||||
format_hex_pretty_to(hex_buf, this->voc_baseline_state_, 4, 0);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Store Baseline: %s\n"
|
||||
" State: %s\n",
|
||||
TRUEFALSE(this->store_baseline_), hex_buf);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
|
||||
LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
|
||||
@@ -304,36 +298,6 @@ void SEN5XComponent::update() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
|
||||
// much
|
||||
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
|
||||
if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
|
||||
// run it a bit later to avoid adding a delay here
|
||||
this->set_timeout(550, [this]() {
|
||||
uint16_t states[4];
|
||||
if (this->read_data(states, 4)) {
|
||||
uint32_t state0 = states[0] << 16 | states[1];
|
||||
uint32_t state1 = states[2] << 16 | states[3];
|
||||
if ((uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state0 - state0)) >
|
||||
MAXIMUM_STORAGE_DIFF ||
|
||||
(uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state1 - state1)) >
|
||||
MAXIMUM_STORAGE_DIFF) {
|
||||
this->seconds_since_last_store_ = 0;
|
||||
this->voc_baselines_storage_.state0 = state0;
|
||||
this->voc_baselines_storage_.state1 = state1;
|
||||
|
||||
if (this->pref_.save(&this->voc_baselines_storage_)) {
|
||||
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
|
||||
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Could not store VOC baselines");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_);
|
||||
@@ -402,7 +366,29 @@ void SEN5XComponent::update() {
|
||||
if (this->nox_sensor_ != nullptr) {
|
||||
this->nox_sensor_->publish_state(nox);
|
||||
}
|
||||
this->status_clear_warning();
|
||||
|
||||
if (!this->voc_sensor_ || !this->store_baseline_ ||
|
||||
(App.get_loop_component_start_time() - this->voc_baseline_time_) < SHORTEST_BASELINE_STORE_INTERVAL) {
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
this->voc_baseline_time_ = App.get_loop_component_start_time();
|
||||
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
} else {
|
||||
this->set_timeout(20, [this]() {
|
||||
if (!this->read_data(this->voc_baseline_state_, 4)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
} else {
|
||||
if (this->pref_.save(&this->voc_baseline_state_)) {
|
||||
ESP_LOGD(TAG, "VOC Baseline State saved");
|
||||
}
|
||||
this->status_clear_warning();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,11 +24,6 @@ enum RhtAccelerationMode : uint16_t {
|
||||
HIGH_ACCELERATION = 2,
|
||||
};
|
||||
|
||||
struct Sen5xBaselines {
|
||||
int32_t state0;
|
||||
int32_t state1;
|
||||
} PACKED; // NOLINT
|
||||
|
||||
struct GasTuning {
|
||||
uint16_t index_offset;
|
||||
uint16_t learning_time_offset_hours;
|
||||
@@ -44,11 +39,9 @@ struct TemperatureCompensation {
|
||||
uint16_t time_constant;
|
||||
};
|
||||
|
||||
// Shortest time interval of 3H for storing baseline values.
|
||||
// Shortest time interval of 2H (in milliseconds) for storing baseline values.
|
||||
// Prevents wear of the flash because of too many write operations
|
||||
static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
|
||||
// Store anyway if the baseline difference exceeds the max storage diff value
|
||||
static const uint32_t MAXIMUM_STORAGE_DIFF = 50;
|
||||
static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 2 * 60 * 60 * 1000;
|
||||
|
||||
class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
public:
|
||||
@@ -107,7 +100,8 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
|
||||
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
|
||||
bool write_temperature_compensation_(const TemperatureCompensation &compensation);
|
||||
|
||||
uint32_t seconds_since_last_store_;
|
||||
uint16_t voc_baseline_state_[4]{0};
|
||||
uint32_t voc_baseline_time_;
|
||||
uint16_t firmware_version_;
|
||||
ERRORCODE error_code_;
|
||||
uint8_t serial_number_[4];
|
||||
@@ -132,7 +126,6 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
|
||||
optional<TemperatureCompensation> temperature_compensation_;
|
||||
ESPPreferenceObject pref_;
|
||||
std::string product_name_;
|
||||
Sen5xBaselines voc_baselines_storage_;
|
||||
};
|
||||
|
||||
} // namespace sen5x
|
||||
|
||||
@@ -210,6 +210,7 @@ SENSOR_MAP = {
|
||||
SETTING_MAP = {
|
||||
CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval",
|
||||
CONF_ACCELERATION_MODE: "set_acceleration_mode",
|
||||
CONF_STORE_BASELINE: "set_store_baseline",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "slow_pwm_output.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace slow_pwm {
|
||||
@@ -20,7 +21,9 @@ void SlowPWMOutput::set_output_state_(bool new_state) {
|
||||
}
|
||||
if (new_state != current_state_) {
|
||||
if (this->pin_) {
|
||||
ESP_LOGV(TAG, "Switching output pin %s to %s", this->pin_->dump_summary().c_str(), ONOFF(new_state));
|
||||
char pin_summary[GPIO_SUMMARY_MAX_LEN];
|
||||
this->pin_->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGV(TAG, "Switching output pin %s to %s", pin_summary, ONOFF(new_state));
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Switching to %s", ONOFF(new_state));
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core", "@clydebarrow"]
|
||||
spi_ns = cg.esphome_ns.namespace("spi")
|
||||
@@ -448,9 +449,13 @@ def spi_device_schema(
|
||||
)
|
||||
|
||||
|
||||
async def register_spi_device(var, config):
|
||||
async def register_spi_device(
|
||||
var: cg.Pvariable, config: ConfigType, write_only: bool = False
|
||||
) -> None:
|
||||
parent = await cg.get_variable(config[CONF_SPI_ID])
|
||||
cg.add(var.set_spi_parent(parent))
|
||||
if write_only:
|
||||
cg.add(var.set_write_only(True))
|
||||
if cs_pin := config.get(CONF_CS_PIN):
|
||||
pin = await cg.gpio_pin_expression(cs_pin)
|
||||
cg.add(var.set_cs_pin(pin))
|
||||
|
||||
@@ -195,8 +195,11 @@ class SPIDelegateHw : public SPIDelegate {
|
||||
config.post_cb = nullptr;
|
||||
if (this->bit_order_ == BIT_ORDER_LSB_FIRST)
|
||||
config.flags |= SPI_DEVICE_BIT_LSBFIRST;
|
||||
if (this->write_only_)
|
||||
if (this->write_only_) {
|
||||
config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY;
|
||||
ESP_LOGD(TAG, "SPI device with CS pin %d using half-duplex mode (write-only)",
|
||||
Utility::get_pin_no(this->cs_pin_));
|
||||
}
|
||||
esp_err_t const err = spi_bus_add_device(this->channel_, &config, &this->handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Add device failed - err %X", err);
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1306_base.setup_ssd1306(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1322_base.setup_ssd1322(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1325_base.setup_ssd1325(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1327_base.setup_ssd1327(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1331_base.setup_ssd1331(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1351_base.setup_ssd1351(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await st7567_base.setup_st7567(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -173,7 +173,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
sequence = []
|
||||
for seq in config[CONF_INIT_SEQUENCE]:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#include "st7701s.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -183,8 +184,11 @@ void ST7701S::dump_config() {
|
||||
LOG_PIN(" DE Pin: ", this->de_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
|
||||
for (size_t i = 0; i != data_pin_count; i++)
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str());
|
||||
char pin_summary[GPIO_SUMMARY_MAX_LEN];
|
||||
for (size_t i = 0; i != data_pin_count; i++) {
|
||||
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
|
||||
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000));
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ async def to_code(config):
|
||||
config[CONF_INVERT_COLORS],
|
||||
)
|
||||
await setup_st7735(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -177,7 +177,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
cg.add(var.set_model_str(config[CONF_MODEL]))
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
if CONF_LAMBDA in config:
|
||||
lambda_ = await cg.process_lambda(
|
||||
|
||||
@@ -40,6 +40,9 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
// Unsigned subtraction handles wraparound correctly, then cast to signed
|
||||
int32_t diff = static_cast<int32_t>(epoch - static_cast<uint32_t>(current_time));
|
||||
if (diff >= -1 && diff <= 1) {
|
||||
// Time is already synchronized, but still call callbacks so components
|
||||
// waiting for time sync (e.g., uptime timestamp sensor) can initialize
|
||||
this->time_sync_callback_.call();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ async def to_code(config):
|
||||
raise NotImplementedError()
|
||||
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -565,6 +565,11 @@ void WiFiComponent::start() {
|
||||
void WiFiComponent::restart_adapter() {
|
||||
ESP_LOGW(TAG, "Restarting adapter");
|
||||
this->wifi_mode_(false, {});
|
||||
// Clear error flag here because restart_adapter() enters COOLDOWN state,
|
||||
// and check_connecting_finished() is called after cooldown without going
|
||||
// through start_connecting() first. Without this clear, stale errors would
|
||||
// trigger spurious "failed (callback)" logs. The canonical clear location
|
||||
// is in start_connecting(); this is the only exception to that pattern.
|
||||
this->error_from_callback_ = false;
|
||||
}
|
||||
|
||||
@@ -618,8 +623,6 @@ void WiFiComponent::loop() {
|
||||
if (!this->is_connected()) {
|
||||
ESP_LOGW(TAG, "Connection lost; reconnecting");
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
|
||||
// Clear error flag before reconnecting so first attempt is not seen as immediate failure
|
||||
this->error_from_callback_ = false;
|
||||
this->retry_connect();
|
||||
} else {
|
||||
this->status_clear_warning();
|
||||
@@ -963,6 +966,12 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden()));
|
||||
#endif
|
||||
|
||||
// Clear any stale error from previous connection attempt.
|
||||
// This is the canonical location for clearing the flag since all connection
|
||||
// attempts go through start_connecting(). The only other clear is in
|
||||
// restart_adapter() which enters COOLDOWN without calling start_connecting().
|
||||
this->error_from_callback_ = false;
|
||||
|
||||
if (!this->wifi_sta_connect_(ap)) {
|
||||
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
|
||||
// Enter cooldown to allow WiFi hardware to stabilize
|
||||
@@ -1068,7 +1077,6 @@ void WiFiComponent::enable() {
|
||||
return;
|
||||
|
||||
ESP_LOGD(TAG, "Enabling");
|
||||
this->error_from_callback_ = false;
|
||||
this->state_ = WIFI_COMPONENT_STATE_OFF;
|
||||
this->start();
|
||||
}
|
||||
@@ -1329,11 +1337,6 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
|
||||
// Reset to initial phase on successful connection (don't log transition, just reset state)
|
||||
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
|
||||
this->num_retried_ = 0;
|
||||
// Ensure next connection attempt does not inherit error state
|
||||
// so when WiFi disconnects later we start fresh and don't see
|
||||
// the first connection as a failure.
|
||||
this->error_from_callback_ = false;
|
||||
|
||||
if (this->has_ap()) {
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
if (this->is_captive_portal_active_()) {
|
||||
@@ -1844,8 +1847,6 @@ void WiFiComponent::retry_connect() {
|
||||
this->advance_to_next_target_or_increment_retry_();
|
||||
}
|
||||
|
||||
this->error_from_callback_ = false;
|
||||
|
||||
yield();
|
||||
// Check if we have a valid target before building params
|
||||
// After exhausting all networks in a phase, selected_sta_index_ may be -1
|
||||
@@ -2171,7 +2172,6 @@ void WiFiComponent::process_roaming_scan_() {
|
||||
this->roaming_state_ = RoamingState::CONNECTING;
|
||||
|
||||
// Connect directly - wifi_sta_connect_ handles disconnect internally
|
||||
this->error_from_callback_ = false;
|
||||
this->start_connecting(roam_params);
|
||||
}
|
||||
|
||||
|
||||
@@ -698,6 +698,10 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
if (!this->wifi_mode_(true, {}))
|
||||
return false;
|
||||
|
||||
// Reset scan_done_ before starting new scan to prevent stale flag from previous scan
|
||||
// (e.g., roaming scan completed just before unexpected disconnect)
|
||||
this->scan_done_ = false;
|
||||
|
||||
struct scan_config config {};
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.ssid = nullptr;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
@@ -827,16 +828,30 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
}
|
||||
|
||||
uint16_t number = it.number;
|
||||
scan_result_.init(number);
|
||||
#ifdef USE_ESP32_HOSTED
|
||||
// getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor
|
||||
// Presumably an upstream bug, work-around by getting all records at once
|
||||
auto records = std::make_unique<wifi_ap_record_t[]>(number);
|
||||
err = esp_wifi_scan_get_ap_records(&number, records.get());
|
||||
if (err != ESP_OK) {
|
||||
esp_wifi_clear_ap_list();
|
||||
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
scan_result_.init(number);
|
||||
for (int i = 0; i < number; i++) {
|
||||
auto &record = records[i];
|
||||
for (uint16_t i = 0; i < number; i++) {
|
||||
wifi_ap_record_t &record = records[i];
|
||||
#else
|
||||
// Process one record at a time to avoid large buffer allocation
|
||||
for (uint16_t i = 0; i < number; i++) {
|
||||
wifi_ap_record_t record;
|
||||
err = esp_wifi_scan_get_ap_record(&record);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_record failed: %s", esp_err_to_name(err));
|
||||
esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved
|
||||
break;
|
||||
}
|
||||
#endif // USE_ESP32_HOSTED
|
||||
bssid_t bssid;
|
||||
std::copy(record.bssid, record.bssid + 6, bssid.begin());
|
||||
std::string ssid(reinterpret_cast<const char *>(record.ssid));
|
||||
|
||||
@@ -460,13 +460,15 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid);
|
||||
}
|
||||
#endif
|
||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||
// For static IP configurations, GOT_IP event may not fire, so set connected state here
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
s_sta_state = LTWiFiSTAState::CONNECTED;
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
@@ -647,6 +649,10 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
if (!this->wifi_mode_(true, {}))
|
||||
return false;
|
||||
|
||||
// Reset scan_done_ before starting new scan to prevent stale flag from previous scan
|
||||
// (e.g., roaming scan completed just before unexpected disconnect)
|
||||
this->scan_done_ = false;
|
||||
|
||||
// need to use WiFi because of WiFiScanClass allocations :(
|
||||
int16_t err = WiFi.scanNetworks(true, true, passive, 200);
|
||||
if (err != WIFI_SCAN_RUNNING) {
|
||||
|
||||
@@ -79,13 +79,17 @@ async def setup_conf(config, key):
|
||||
|
||||
async def to_code(config):
|
||||
# Request specific WiFi listeners based on which sensors are configured
|
||||
# Each sensor needs its own listener slot - call request for EACH sensor
|
||||
|
||||
# SSID and BSSID use WiFiConnectStateListener
|
||||
if CONF_SSID in config or CONF_BSSID in config:
|
||||
wifi.request_wifi_connect_state_listener()
|
||||
for key in (CONF_SSID, CONF_BSSID):
|
||||
if key in config:
|
||||
wifi.request_wifi_connect_state_listener()
|
||||
|
||||
# IP address and DNS use WiFiIPStateListener
|
||||
if CONF_IP_ADDRESS in config or CONF_DNS_ADDRESS in config:
|
||||
wifi.request_wifi_ip_state_listener()
|
||||
for key in (CONF_IP_ADDRESS, CONF_DNS_ADDRESS):
|
||||
if key in config:
|
||||
wifi.request_wifi_ip_state_listener()
|
||||
|
||||
# Scan results use WiFiScanResultsListener
|
||||
if CONF_SCAN_RESULTS in config:
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace x9c {
|
||||
|
||||
static const char *const TAG = "x9c.output";
|
||||
|
||||
void X9cOutput::trim_value(int change_amount) {
|
||||
void X9cOutput::trim_value(int32_t change_amount) {
|
||||
if (change_amount == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -47,17 +47,17 @@ void X9cOutput::setup() {
|
||||
|
||||
if (this->initial_value_ <= 0.50) {
|
||||
this->trim_value(-101); // Set min value (beyond 0)
|
||||
this->trim_value(static_cast<uint32_t>(roundf(this->initial_value_ * 100)));
|
||||
this->trim_value(lroundf(this->initial_value_ * 100));
|
||||
} else {
|
||||
this->trim_value(101); // Set max value (beyond 100)
|
||||
this->trim_value(static_cast<uint32_t>(roundf(this->initial_value_ * 100) - 100));
|
||||
this->trim_value(lroundf(this->initial_value_ * 100) - 100);
|
||||
}
|
||||
this->pot_value_ = this->initial_value_;
|
||||
this->write_state(this->initial_value_);
|
||||
}
|
||||
|
||||
void X9cOutput::write_state(float state) {
|
||||
this->trim_value(static_cast<uint32_t>(roundf((state - this->pot_value_) * 100)));
|
||||
this->trim_value(lroundf((state - this->pot_value_) * 100));
|
||||
this->pot_value_ = state;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class X9cOutput : public output::FloatOutput, public Component {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void trim_value(int change_amount);
|
||||
void trim_value(int32_t change_amount);
|
||||
|
||||
protected:
|
||||
void write_state(float state) override;
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.1.0b3"
|
||||
__version__ = "2026.1.2"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#define USE_DEVICES
|
||||
#define USE_DISPLAY
|
||||
#define USE_ENTITY_ICON
|
||||
#define USE_ESP32_HOSTED
|
||||
#define USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
#define USE_EVENT
|
||||
#define USE_FAN
|
||||
|
||||
@@ -362,6 +362,35 @@ template<typename T> class FixedVector {
|
||||
const T *end() const { return data_ + size_; }
|
||||
};
|
||||
|
||||
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
|
||||
/// This is useful when most operations need a small buffer but occasionally need larger ones.
|
||||
/// The stack buffer avoids heap allocation in the common case, while heap fallback handles edge cases.
|
||||
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
|
||||
public:
|
||||
explicit SmallBufferWithHeapFallback(size_t size) {
|
||||
if (size <= STACK_SIZE) {
|
||||
this->buffer_ = this->stack_buffer_;
|
||||
} else {
|
||||
this->heap_buffer_ = new uint8_t[size];
|
||||
this->buffer_ = this->heap_buffer_;
|
||||
}
|
||||
}
|
||||
~SmallBufferWithHeapFallback() { delete[] this->heap_buffer_; }
|
||||
|
||||
// Delete copy and move operations to prevent double-delete
|
||||
SmallBufferWithHeapFallback(const SmallBufferWithHeapFallback &) = delete;
|
||||
SmallBufferWithHeapFallback &operator=(const SmallBufferWithHeapFallback &) = delete;
|
||||
SmallBufferWithHeapFallback(SmallBufferWithHeapFallback &&) = delete;
|
||||
SmallBufferWithHeapFallback &operator=(SmallBufferWithHeapFallback &&) = delete;
|
||||
|
||||
uint8_t *get() { return this->buffer_; }
|
||||
|
||||
private:
|
||||
uint8_t stack_buffer_[STACK_SIZE];
|
||||
uint8_t *heap_buffer_{nullptr};
|
||||
uint8_t *buffer_;
|
||||
};
|
||||
|
||||
///@}
|
||||
|
||||
/// @name Mathematics
|
||||
|
||||
18
tests/components/ir_rf_proxy/common-rx.yaml
Normal file
18
tests/components/ir_rf_proxy/common-rx.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
remote_receiver:
|
||||
id: ir_receiver
|
||||
pin: ${rx_pin}
|
||||
|
||||
# Test various hardware types with transmitter/receiver using infrared platform
|
||||
infrared:
|
||||
# Infrared receiver
|
||||
- platform: ir_rf_proxy
|
||||
id: ir_rx
|
||||
name: "IR Receiver"
|
||||
remote_receiver_id: ir_receiver
|
||||
|
||||
# RF 900MHz receiver
|
||||
- platform: ir_rf_proxy
|
||||
id: rf_900_rx
|
||||
name: "RF 900 Receiver"
|
||||
frequency: 900 MHz
|
||||
remote_receiver_id: ir_receiver
|
||||
19
tests/components/ir_rf_proxy/common-tx.yaml
Normal file
19
tests/components/ir_rf_proxy/common-tx.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
remote_transmitter:
|
||||
id: ir_transmitter
|
||||
pin: ${tx_pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
# Test various hardware types with transmitter/receiver using infrared platform
|
||||
infrared:
|
||||
# Infrared transmitter
|
||||
- platform: ir_rf_proxy
|
||||
id: ir_tx
|
||||
name: "IR Transmitter"
|
||||
remote_transmitter_id: ir_transmitter
|
||||
|
||||
# RF 433MHz transmitter
|
||||
- platform: ir_rf_proxy
|
||||
id: rf_433_tx
|
||||
name: "RF 433 Transmitter"
|
||||
frequency: 433 MHz
|
||||
remote_transmitter_id: ir_transmitter
|
||||
@@ -1,42 +1,7 @@
|
||||
network:
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
api:
|
||||
|
||||
remote_transmitter:
|
||||
id: ir_transmitter
|
||||
pin: ${tx_pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
remote_receiver:
|
||||
id: ir_receiver
|
||||
pin: ${rx_pin}
|
||||
|
||||
# Test various hardware types with transmitter/receiver using infrared platform
|
||||
infrared:
|
||||
# Infrared transmitter
|
||||
- platform: ir_rf_proxy
|
||||
id: ir_tx
|
||||
name: "IR Transmitter"
|
||||
remote_transmitter_id: ir_transmitter
|
||||
|
||||
# Infrared receiver
|
||||
- platform: ir_rf_proxy
|
||||
id: ir_rx
|
||||
name: "IR Receiver"
|
||||
remote_receiver_id: ir_receiver
|
||||
|
||||
# RF 433MHz transmitter
|
||||
- platform: ir_rf_proxy
|
||||
id: rf_433_tx
|
||||
name: "RF 433 Transmitter"
|
||||
frequency: 433 MHz
|
||||
remote_transmitter_id: ir_transmitter
|
||||
|
||||
# RF 900MHz receiver
|
||||
- platform: ir_rf_proxy
|
||||
id: rf_900_rx
|
||||
name: "RF 900 Receiver"
|
||||
frequency: 900 MHz
|
||||
remote_receiver_id: ir_receiver
|
||||
|
||||
7
tests/components/ir_rf_proxy/test-rx.esp32-idf.yaml
Normal file
7
tests/components/ir_rf_proxy/test-rx.esp32-idf.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
7
tests/components/ir_rf_proxy/test-rx.esp8266-ard.yaml
Normal file
7
tests/components/ir_rf_proxy/test-rx.esp8266-ard.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
7
tests/components/ir_rf_proxy/test-rx.rp2040-ard.yaml
Normal file
7
tests/components/ir_rf_proxy/test-rx.rp2040-ard.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
7
tests/components/ir_rf_proxy/test-tx.esp32-idf.yaml
Normal file
7
tests/components/ir_rf_proxy/test-tx.esp32-idf.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
tx: !include common-tx.yaml
|
||||
7
tests/components/ir_rf_proxy/test-tx.esp8266-ard.yaml
Normal file
7
tests/components/ir_rf_proxy/test-tx.esp8266-ard.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
tx: !include common-tx.yaml
|
||||
7
tests/components/ir_rf_proxy/test-tx.rp2040-ard.yaml
Normal file
7
tests/components/ir_rf_proxy/test-tx.rp2040-ard.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
tx: !include common-tx.yaml
|
||||
8
tests/components/ir_rf_proxy/test.bk72xx-ard.yaml
Normal file
8
tests/components/ir_rf_proxy/test.bk72xx-ard.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
tx: !include common-tx.yaml
|
||||
@@ -2,4 +2,7 @@ substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
tx: !include common-tx.yaml
|
||||
|
||||
@@ -2,4 +2,7 @@ substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
tx: !include common-tx.yaml
|
||||
|
||||
@@ -2,4 +2,7 @@ substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
tx: !include common-tx.yaml
|
||||
|
||||
@@ -197,6 +197,9 @@ lvgl:
|
||||
- lvgl.label.update:
|
||||
id: msgbox_label
|
||||
text: Unloaded
|
||||
- lvgl.label.update:
|
||||
id: msgbox_label
|
||||
text: "" # Empty text
|
||||
on_all_events:
|
||||
logger.log:
|
||||
format: "Event %s"
|
||||
|
||||
@@ -108,6 +108,25 @@ text_sensor:
|
||||
format: "HA Empty state updated: %s"
|
||||
args: ['x.c_str()']
|
||||
|
||||
# Test long attribute handling (>255 characters)
|
||||
# HA states are limited to 255 chars, but attributes are not
|
||||
- platform: homeassistant
|
||||
name: "HA Long Attribute"
|
||||
entity_id: sensor.long_data
|
||||
attribute: long_value
|
||||
id: ha_long_attribute
|
||||
on_value:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "HA Long attribute received, length: %d"
|
||||
args: ['x.size()']
|
||||
# Log the first 50 and last 50 chars to verify no truncation
|
||||
- lambda: |-
|
||||
if (x.size() >= 100) {
|
||||
ESP_LOGI("test", "Long attribute first 50 chars: %.50s", x.c_str());
|
||||
ESP_LOGI("test", "Long attribute last 50 chars: %s", x.c_str() + x.size() - 50);
|
||||
}
|
||||
|
||||
# Number component for testing HA number control
|
||||
number:
|
||||
- platform: template
|
||||
|
||||
@@ -40,6 +40,7 @@ async def test_api_homeassistant(
|
||||
humidity_update_future = loop.create_future()
|
||||
motion_update_future = loop.create_future()
|
||||
weather_update_future = loop.create_future()
|
||||
long_attr_future = loop.create_future()
|
||||
|
||||
# Number future
|
||||
ha_number_future = loop.create_future()
|
||||
@@ -58,6 +59,7 @@ async def test_api_homeassistant(
|
||||
humidity_update_pattern = re.compile(r"HA Humidity state updated: ([\d.]+)")
|
||||
motion_update_pattern = re.compile(r"HA Motion state changed: (ON|OFF)")
|
||||
weather_update_pattern = re.compile(r"HA Weather condition updated: (\w+)")
|
||||
long_attr_pattern = re.compile(r"HA Long attribute received, length: (\d+)")
|
||||
|
||||
# Number pattern
|
||||
ha_number_pattern = re.compile(r"Setting HA number to: ([\d.]+)")
|
||||
@@ -143,8 +145,14 @@ async def test_api_homeassistant(
|
||||
elif not weather_update_future.done() and weather_update_pattern.search(line):
|
||||
weather_update_future.set_result(line)
|
||||
|
||||
# Check number pattern
|
||||
elif not ha_number_future.done() and ha_number_pattern.search(line):
|
||||
# Check long attribute pattern - separate if since it can come at different times
|
||||
if not long_attr_future.done():
|
||||
match = long_attr_pattern.search(line)
|
||||
if match:
|
||||
long_attr_future.set_result(int(match.group(1)))
|
||||
|
||||
# Check number pattern - separate if since it can come at different times
|
||||
if not ha_number_future.done():
|
||||
match = ha_number_pattern.search(line)
|
||||
if match:
|
||||
ha_number_future.set_result(match.group(1))
|
||||
@@ -179,6 +187,14 @@ async def test_api_homeassistant(
|
||||
client.send_home_assistant_state("binary_sensor.external_motion", "", "ON")
|
||||
client.send_home_assistant_state("weather.home", "condition", "sunny")
|
||||
|
||||
# Send a long attribute (300 characters) to test that attributes aren't truncated
|
||||
# HA states are limited to 255 chars, but attributes are NOT limited
|
||||
# This tests the fix for the 256-byte buffer truncation bug
|
||||
long_attr_value = "X" * 300 # 300 chars - enough to expose truncation bug
|
||||
client.send_home_assistant_state(
|
||||
"sensor.long_data", "long_value", long_attr_value
|
||||
)
|
||||
|
||||
# Test edge cases for zero-copy implementation safety
|
||||
# Empty entity_id should be silently ignored (no crash)
|
||||
client.send_home_assistant_state("", "", "should_be_ignored")
|
||||
@@ -225,6 +241,13 @@ async def test_api_homeassistant(
|
||||
number_value = await asyncio.wait_for(ha_number_future, timeout=5.0)
|
||||
assert number_value == "42.5", f"Unexpected number value: {number_value}"
|
||||
|
||||
# Long attribute test - verify 300 chars weren't truncated to 255
|
||||
long_attr_len = await asyncio.wait_for(long_attr_future, timeout=5.0)
|
||||
assert long_attr_len == 300, (
|
||||
f"Long attribute was truncated! Expected 300 chars, got {long_attr_len}. "
|
||||
"This indicates the 256-byte truncation bug."
|
||||
)
|
||||
|
||||
# Wait for completion
|
||||
await asyncio.wait_for(tests_complete_future, timeout=5.0)
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ from esphome.__main__ import (
|
||||
has_non_ip_address,
|
||||
has_resolvable_address,
|
||||
mqtt_get_ip,
|
||||
run_esphome,
|
||||
run_miniterm,
|
||||
show_logs,
|
||||
upload_program,
|
||||
@@ -1988,7 +1989,7 @@ esp32:
|
||||
clean_output = strip_ansi_codes(captured.out)
|
||||
|
||||
assert "test-device_123.yaml" in clean_output
|
||||
assert "Updating" in clean_output
|
||||
assert "Processing" in clean_output
|
||||
assert "SUCCESS" in clean_output
|
||||
assert "SUMMARY" in clean_output
|
||||
|
||||
@@ -3172,3 +3173,66 @@ def test_run_miniterm_buffer_limit_prevents_unbounded_growth() -> None:
|
||||
x_count = printed_line.count("X")
|
||||
assert x_count < 150, f"Expected truncation but got {x_count} X's"
|
||||
assert x_count == 95, f"Expected 95 X's after truncation but got {x_count}"
|
||||
|
||||
|
||||
def test_run_esphome_multiple_configs_with_secrets(
|
||||
tmp_path: Path,
|
||||
mock_run_external_process: Mock,
|
||||
capfd: CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test run_esphome with multiple configs and secrets file.
|
||||
|
||||
Verifies:
|
||||
- Multiple configs use subprocess isolation
|
||||
- Secrets files are skipped with warning
|
||||
- Secrets files don't appear in summary
|
||||
"""
|
||||
# Create two config files and a secrets file
|
||||
yaml_file1 = tmp_path / "device1.yaml"
|
||||
yaml_file1.write_text("""
|
||||
esphome:
|
||||
name: device1
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
""")
|
||||
yaml_file2 = tmp_path / "device2.yaml"
|
||||
yaml_file2.write_text("""
|
||||
esphome:
|
||||
name: device2
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
""")
|
||||
secrets_file = tmp_path / "secrets.yaml"
|
||||
secrets_file.write_text("wifi_password: secret123\n")
|
||||
|
||||
setup_core(tmp_path=tmp_path)
|
||||
mock_run_external_process.return_value = 0
|
||||
|
||||
# run_esphome expects argv[0] to be the program name (gets sliced off by parse_args)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
result = run_esphome(
|
||||
["esphome", "compile", str(yaml_file1), str(secrets_file), str(yaml_file2)]
|
||||
)
|
||||
|
||||
assert result == 0
|
||||
|
||||
# Check secrets file was skipped with warning
|
||||
assert "Skipping secrets file" in caplog.text
|
||||
assert "secrets.yaml" in caplog.text
|
||||
|
||||
captured = capfd.readouterr()
|
||||
clean_output = strip_ansi_codes(captured.out)
|
||||
|
||||
# Both config files should be processed
|
||||
assert "device1.yaml" in clean_output
|
||||
assert "device2.yaml" in clean_output
|
||||
assert "SUMMARY" in clean_output
|
||||
|
||||
# Secrets should not appear in summary
|
||||
summary_section = (
|
||||
clean_output.split("SUMMARY")[1] if "SUMMARY" in clean_output else ""
|
||||
)
|
||||
assert "secrets.yaml" not in summary_section
|
||||
|
||||
Reference in New Issue
Block a user