Compare commits

..
50 changed files with 109 additions and 462 deletions
+3 -3
View File
@@ -829,9 +829,9 @@ def write_cpp_file() -> int:
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
if CORE.skip_bootloader and not (CORE.is_esp32 and CORE.using_toolchain_esp_idf):
# Debug only: an orchestrator cannot see YAML toolchain overrides,
# so this is its expected no-op, and a full build is safe.
_LOGGER.debug(
# Info, not a warning: an orchestrator cannot see YAML toolchain
# overrides, this is its expected no-op, and a full build is safe.
_LOGGER.info(
"--skip-bootloader ignored: only supported on ESP32 with the "
"esp-idf toolchain"
)
+2 -5
View File
@@ -142,7 +142,6 @@ CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
DEFAULT_PORT = 6053
DEFAULT_REBOOT_TIMEOUT = "15min"
DEFAULT_BATCH_DELAY = "100ms"
DEFAULT_LISTEN_BACKLOG = 4
def _register_provisioning_source(config: ConfigType) -> ConfigType:
@@ -478,10 +477,8 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_reboot_timeout(reboot_timeout))
if (batch_delay := config[CONF_BATCH_DELAY]) != cv.time_period(DEFAULT_BATCH_DELAY):
cg.add(var.set_batch_delay(batch_delay))
if (
listen_backlog := config.get(CONF_LISTEN_BACKLOG)
) is not None and listen_backlog != DEFAULT_LISTEN_BACKLOG:
cg.add(var.set_listen_backlog(listen_backlog))
if CONF_LISTEN_BACKLOG in config:
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
+3 -1
View File
@@ -357,7 +357,9 @@ class APIServer final : public Component
// Group smaller types together
uint16_t port_{6053}; // Keep in sync with DEFAULT_PORT in __init__.py
uint16_t batch_delay_{100}; // Keep in sync with DEFAULT_BATCH_DELAY in __init__.py
uint8_t listen_backlog_{4}; // Keep in sync with DEFAULT_LISTEN_BACKLOG in __init__.py
// Connection limits - these defaults will be overridden by config values
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
uint8_t listen_backlog_{4};
bool shutting_down_ = false;
uint8_t api_connection_count_{0};
#if defined(USE_PROVISIONING) && defined(USE_API_NOISE)
+6 -4
View File
@@ -17,7 +17,8 @@ extern "C" __attribute__((weak)) void initArduino() {}
namespace esphome {
// HAL functions live in hal.cpp. This file keeps only the loop task setup.
static StaticTask_t loop_task_tcb; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static StaticTask_t loop_task_tcb; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static StackType_t
loop_task_stack[ESPHOME_LOOP_TASK_STACK_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -39,10 +40,11 @@ extern "C" void app_main() {
initArduino();
esp32::setup_preferences();
#if CONFIG_FREERTOS_UNICORE
xTaskCreateStatic(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, loop_task_stack, &loop_task_tcb);
loop_task_handle = xTaskCreateStatic(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, loop_task_stack,
&loop_task_tcb);
#else
xTaskCreateStaticPinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, loop_task_stack,
&loop_task_tcb, 1);
loop_task_handle = xTaskCreateStaticPinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1,
loop_task_stack, &loop_task_tcb, 1);
#endif
}
+3 -3
View File
@@ -235,14 +235,14 @@ class ESP32BLE final : public Component {
#ifdef USE_ESP32_BLE_ADVERTISING
BLEAdvertising *advertising_{}; // 4 bytes (pointer)
#endif
const char *name_{nullptr}; // 4 bytes (pointer to string literal in flash)
uint32_t advertising_cycle_time_{}; // 4 bytes
const char *name_{nullptr}; // 4 bytes (pointer to string literal in flash)
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum)
uint32_t advertising_cycle_time_{}; // 4 bytes
// 2-byte aligned members
uint16_t appearance_{0}; // 2 bytes
// 1-byte aligned members (grouped together to minimize padding)
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 1 byte (uint8_t typedef)
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum)
bool enable_on_boot_{}; // 1 byte
#ifdef USE_ESP32_BLE_ADVERTISING
@@ -72,6 +72,9 @@ class ImprovBLEComponent final : public Component, public improv_base::ImprovBas
improv::Error get_improv_error_state() const { return this->error_state_; }
protected:
bool should_start_{false};
bool setup_complete_{false};
uint32_t identify_start_{0};
uint32_t identify_duration_;
uint32_t authorized_start_{0};
@@ -81,14 +84,6 @@ class ImprovBLEComponent final : public Component, public improv_base::ImprovBas
std::vector<uint8_t> incoming_data_;
wifi::WiFiAP connecting_sta_;
// Single byte fields here fill the alignment gap between connecting_sta_ and the next pointer
improv::State state_{improv::STATE_STOPPED};
improv::Error error_state_{improv::ERROR_NONE};
bool should_start_{false};
bool setup_complete_{false};
bool status_indicator_state_{false};
bool advertising_device_name_{false};
bool advertising_requested_{false};
BLEService *service_{nullptr};
BLECharacteristic *status_{nullptr};
@@ -104,11 +99,16 @@ class ImprovBLEComponent final : public Component, public improv_base::ImprovBas
output::BinaryOutput *status_indicator_{nullptr};
#endif
improv::State state_{improv::STATE_STOPPED};
improv::Error error_state_{improv::ERROR_NONE};
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
CallbackManager<void(improv::State, improv::Error)> state_callback_{};
#endif
bool status_indicator_state_{false};
uint32_t last_name_adv_time_{0};
bool advertising_device_name_{false};
bool advertising_requested_{false};
void set_status_indicator_state_(bool state);
void update_advertising_type_();
void request_advertising_();
+5
View File
@@ -32,6 +32,7 @@ from esphome.storage_json import StorageJSON
from . import gpio # noqa: F401
from .const import (
COMPONENT_BK72XX,
COMPONENT_LN882X,
CONF_GPIO_RECOVER,
CONF_LOGLEVEL,
CONF_SDK_SILENT,
@@ -490,6 +491,10 @@ async def component_to_code(config):
# Not enabled on RTL87xx/LN882x — costs more heap than it saves there.
cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1")
# LN882x: a zero-size allocation must not trip the SDK's assert (see ln882x_zero_malloc.c).
if config[CONF_COMPONENT_ID] == COMPONENT_LN882X:
cg.add_build_flag("-Wl,--wrap=pvPortMalloc")
# RTL8710B needs FreeRTOS 8.2.3+ for xTaskNotifyGive/ulTaskNotifyTake
# required by AsyncTCP 3.4.3+ (https://github.com/esphome/esphome/issues/10220)
# RTL8720C (ambz2) requires FreeRTOS 10.x so this only applies to RTL8710B
@@ -0,0 +1,13 @@
// LN882x: a zero-size pvPortMalloc trips the SDK's malloc-failed LN_ASSERT; ask for 1 byte instead.
#ifdef USE_LN882X
#include <stddef.h>
void *__real_pvPortMalloc(size_t size); // NOLINT(readability-identifier-naming)
void *__wrap_pvPortMalloc(size_t size) { // NOLINT(readability-identifier-naming)
return __real_pvPortMalloc(size == 0 ? 1 : size);
}
#endif // USE_LN882X
+2 -9
View File
@@ -27,10 +27,6 @@ CONF_HAS_PULLDOWNS = "has_pulldowns"
def check_keys(obj: ConfigType) -> ConfigType:
for ch in obj.get(CONF_KEYS, ""):
if not ch.isascii():
# Each key is reported as one byte, so only ASCII characters can be key codes
raise cv.Invalid(f"Key code {ch!r} is not an ASCII character")
if CONF_KEYS in obj and len(obj[CONF_KEYS]) != len(obj[CONF_ROWS]) * len(
obj[CONF_COLUMNS]
):
@@ -78,11 +74,8 @@ async def to_code(config: ConfigType) -> None:
pin = await cg.gpio_pin_expression(conf[CONF_PIN])
col_pins.append(pin)
cg.add(var.set_columns(col_pins))
if (keys := config.get(CONF_KEYS)) is not None:
table = cg.shared_progmem_array(
"matrix_keypad_keys", cg.uint8, list(keys.encode())
)
cg.add(var.set_keys(table))
if CONF_KEYS in config:
cg.add(var.set_keys(config[CONF_KEYS]))
cg.add(var.set_debounce_time(config[CONF_DEBOUNCE_TIME]))
if CONF_HAS_DIODES in config:
cg.add(var.set_has_diodes(config[CONF_HAS_DIODES]))
@@ -58,8 +58,8 @@ void MatrixKeypad::loop() {
ESP_LOGD(TAG, "key @ row %d, col %d released", row, col);
for (auto &listener : this->listeners_)
listener->button_released(row, col);
if (this->keys_ != nullptr) {
uint8_t keycode = progmem_read_byte(&this->keys_[this->pressed_key_]);
if (this->pressed_key_ < (int) this->keys_.size()) {
uint8_t keycode = this->keys_[this->pressed_key_];
ESP_LOGD(TAG, "key '%c' released", keycode);
for (auto &listener : this->listeners_)
listener->key_released(keycode);
@@ -81,8 +81,8 @@ void MatrixKeypad::loop() {
ESP_LOGD(TAG, "key @ row %d, col %d pressed", row, col);
for (auto &listener : this->listeners_)
listener->button_pressed(row, col);
if (this->keys_ != nullptr) {
uint8_t keycode = progmem_read_byte(&this->keys_[key]);
if (key < (int) this->keys_.size()) {
uint8_t keycode = this->keys_[key];
ESP_LOGD(TAG, "key '%c' pressed", keycode);
for (auto &trigger : this->key_triggers_)
trigger->trigger(keycode);
@@ -27,8 +27,7 @@ class MatrixKeypad final : public key_provider::KeyProvider, public Component {
void dump_config() override;
void set_columns(std::vector<GPIOPin *> pins) { columns_ = std::move(pins); };
void set_rows(std::vector<GPIOPin *> pins) { rows_ = std::move(pins); };
/// `keys` is a codegen PROGMEM table with one key code per button.
void set_keys(const uint8_t *keys) { this->keys_ = keys; }
void set_keys(std::string keys) { keys_ = std::move(keys); };
void set_debounce_time(uint32_t debounce_time) { debounce_time_ = debounce_time; };
void set_has_diodes(bool has_diodes) { has_diodes_ = has_diodes; };
void set_has_pulldowns(bool has_pulldowns) { has_pulldowns_ = has_pulldowns; };
@@ -39,7 +38,7 @@ class MatrixKeypad final : public key_provider::KeyProvider, public Component {
protected:
std::vector<GPIOPin *> rows_;
std::vector<GPIOPin *> columns_;
const uint8_t *keys_{nullptr};
std::string keys_;
uint32_t debounce_time_ = 0;
bool has_diodes_{false};
bool has_pulldowns_{false};
@@ -88,8 +88,8 @@ class RemoteReceiverComponent final : public remote_base::RemoteReceiverBase,
rmt_channel_handle_t channel_{NULL};
uint32_t filter_symbols_{0};
uint32_t receive_symbols_{0};
uint32_t carrier_frequency_{0};
bool with_dma_{false};
uint32_t carrier_frequency_{0};
uint8_t carrier_duty_percent_{100};
#endif
+1 -2
View File
@@ -254,8 +254,7 @@ template<class... As, typename... Ts> class ScriptExecuteAction<Script<As...>, T
}
Script<As...> *script_;
// Empty tuple for scripts without parameters; takes no space then
[[no_unique_address]] Args args_;
Args args_;
};
/** Wait for a script to finish before continuing.
+8 -2
View File
@@ -187,8 +187,14 @@ automation.register_apply_action(
async def select_is_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
if options := config.get(CONF_OPTIONS):
# Shared flash table of option pointers, length passed as a template argument
arg = cg.shared_progmem_array("select_is_options", cg.const_char_ptr, options)
# List of constant options
# Create a constexpr and pass that with a template length
arr_id = ID(
f"{condition_id}_data",
is_declaration=True,
type=global_ns.namespace("constexpr char * const"),
)
arg = cg.static_const_array(arr_id, cg.ArrayInitializer(*options))
template_arg = TemplateArguments(len(options), *template_arg)
else:
# Lambda
@@ -9,8 +9,7 @@
namespace esphome::template_ {
// Distinct per member: two empty members of the same type cannot share an address
template<int N> struct Empty {};
struct Empty {};
class BaseTemplateSelect : public select::Select, public PollingComponent {};
void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, size_t initial_option_index,
@@ -60,8 +59,8 @@ class TemplateSelect : public BaseTemplateSelect {
if constexpr (RESTORE_VALUE)
this->pref_.save(&index);
}
[[no_unique_address]] std::conditional_t<HAS_LAMBDA, TemplateLambda<std::string>, Empty<0>> f_{};
[[no_unique_address]] std::conditional_t<RESTORE_VALUE, ESPPreferenceObject, Empty<1>> pref_{};
[[no_unique_address]] std::conditional_t<HAS_LAMBDA, TemplateLambda<std::string>, Empty> f_{};
[[no_unique_address]] std::conditional_t<RESTORE_VALUE, ESPPreferenceObject, Empty> pref_{};
};
/// Template select with set_action trigger - only instantiated when set_action is configured
+1 -3
View File
@@ -402,9 +402,7 @@ async def to_code(config):
cg.add(var.set_clock_source(UART_CLOCK_SOURCES[clock_source]))
cg.add(var.set_stop_bits(config[CONF_STOP_BITS]))
cg.add(var.set_data_bits(config[CONF_DATA_BITS]))
# Skip the setter when the config matches the C++ initializer (UART_CONFIG_PARITY_NONE).
if (parity := config[CONF_PARITY]) != "NONE":
cg.add(var.set_parity(parity))
cg.add(var.set_parity(config[CONF_PARITY]))
if CONF_DEBUG in config:
await debug_to_code(config[CONF_DEBUG], var)
+1 -1
View File
@@ -217,7 +217,7 @@ class UARTComponent {
uint32_t baud_rate_{0};
uint8_t stop_bits_{0};
uint8_t data_bits_{0};
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE}; // Must match the parity check in __init__.py
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
#ifdef USE_UART_DEBUGGER
CallbackManager<void(UARTDirection, uint8_t)> debug_callback_{};
#endif
+2 -5
View File
@@ -35,8 +35,5 @@ CONFIG_SCHEMA = (
async def to_code(config: ConfigType) -> None:
var = await text_sensor.new_text_sensor(config)
await cg.register_component(var, config)
# Both flags default to false in C++, so only true needs a setter.
if config[CONF_HIDE_HASH]:
cg.add(var.set_hide_hash(True))
if config[CONF_HIDE_TIMESTAMP]:
cg.add(var.set_hide_timestamp(True))
cg.add(var.set_hide_hash(config[CONF_HIDE_HASH]))
cg.add(var.set_hide_timestamp(config[CONF_HIDE_TIMESTAMP]))
@@ -15,8 +15,8 @@ class VersionTextSensor final : public text_sensor::TextSensor, public Component
void dump_config() override;
protected:
bool hide_hash_{false}; // Must match the setter check in text_sensor.py
bool hide_timestamp_{false}; // Must match the setter check in text_sensor.py
bool hide_hash_{false};
bool hide_timestamp_{false};
};
} // namespace esphome::version
+1 -10
View File
@@ -413,16 +413,7 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
if (allowed_origins := config.get(CONF_ALLOWED_ORIGINS)) is not None:
cg.add_define("USE_WEBSERVER_ALLOWED_ORIGINS")
# Shared flash table ended by nullptr, so the server stores only a pointer.
cg.add(
var.set_allowed_origins(
cg.shared_progmem_array(
"web_server_allowed_origins",
cg.const_char_ptr,
[*allowed_origins, cg.nullptr],
)
)
)
cg.add(var.set_allowed_origins(allowed_origins))
if (auth := config.get(CONF_AUTH)) is not None:
cg.add_define("USE_WEBSERVER_AUTH")
# The scheme is fixed at build time so the unused Basic/Digest code path is compiled
+1 -2
View File
@@ -495,8 +495,7 @@ bool WebServer::is_request_origin_allowed_(AsyncWebServerRequest *request, const
#ifdef USE_WEBSERVER_ALLOWED_ORIGINS
// Otherwise the origin must be explicitly allowed via configuration.
for (const char *const *it = this->allowed_origins_; *it != nullptr; it++) {
const char *allowed_origin = *it;
for (const char *allowed_origin : this->allowed_origins_) {
// A single "*" entry allows any origin.
if (allowed_origin[0] == '*' && allowed_origin[1] == '\0')
return true;
+4 -6
View File
@@ -254,9 +254,9 @@ class WebServer final : public Component, public AsyncWebHandler {
*
* This list is also used to authorize Private Network Access requests when that feature is enabled.
*
* @param origins nullptr-terminated table of allowed origins; must outlive the server.
* @param origins The list of allowed origins.
*/
void set_allowed_origins(const char *const *origins) { this->allowed_origins_ = origins; }
void set_allowed_origins(std::initializer_list<const char *> origins) { this->allowed_origins_ = origins; }
#endif
// ========== INTERNAL METHODS ==========
@@ -531,9 +531,6 @@ class WebServer final : public Component, public AsyncWebHandler {
bool include_internal_{false};
protected:
// Declared here so it packs into the padding after include_internal_
bool expose_log_{true};
void add_sorting_info_(JsonObject &root, EntityBase *entity);
#ifdef USE_LIGHT
@@ -617,10 +614,11 @@ class WebServer final : public Component, public AsyncWebHandler {
#ifdef USE_WEBSERVER_JS_INCLUDE
const char *js_include_{nullptr};
#endif
bool expose_log_{true};
#ifdef USE_WEBSERVER_ALLOWED_ORIGINS
// Extra origins allowed to make cross-origin browser requests ("*" means any origin).
// Only compiled when allowed_origins is configured; same-origin is always allowed regardless.
const char *const *allowed_origins_{nullptr};
FixedVector<const char *> allowed_origins_;
#endif
/// Check whether the given request Origin is permitted. Same-origin (matching the Host the
@@ -761,7 +761,7 @@ void AsyncEventSource::adopt_pending_sessions_main_loop_() {
this->has_pending_sessions_.store(false, std::memory_order_relaxed);
}
for (auto *rsp : incoming) {
// Already disconnected? Drop it; skip session start on a dead session.
// Already disconnected? Drop it; skip on_connect_/session start on a dead session.
if (rsp->safe_to_delete_()) {
delete rsp; // NOLINT(cppcoreguidelines-owning-memory)
continue;
@@ -775,7 +775,12 @@ void AsyncEventSource::adopt_pending_sessions_main_loop_() {
continue;
}
this->sessions_.push_back(rsp);
// Prime first so on_connect_ observes a session that has already sent its
// initial ping/config/sorting_groups, matching the pre-refactor ordering.
rsp->start_session_main_loop_();
if (this->on_connect_) {
this->on_connect_(rsp);
}
}
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
@@ -379,6 +379,7 @@ using AsyncEventSourceClient = AsyncEventSourceResponse;
class AsyncEventSource : public AsyncWebHandler {
friend class AsyncEventSourceResponse;
using connect_handler_t = std::function<void(AsyncEventSourceClient *)>;
public:
AsyncEventSource(StringRef url, esphome::web_server::WebServer *ws) : url_(url), web_server_(ws) {}
@@ -393,6 +394,11 @@ class AsyncEventSource : public AsyncWebHandler {
}
// NOLINTNEXTLINE(readability-identifier-naming)
void handleRequest(AsyncWebServerRequest *request) override;
// Callback runs on the main loop (not the httpd task) after the session's
// initial ping/config/sorting_groups have been sent.
// NOLINTNEXTLINE(readability-identifier-naming)
void onConnect(connect_handler_t &&cb) { this->on_connect_ = std::move(cb); }
void try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
uint32_t reconnect = 0);
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
@@ -412,6 +418,7 @@ class AsyncEventSource : public AsyncWebHandler {
// Httpd-task intake; guarded by pending_mutex_, gated by has_pending_sessions_.
std::vector<AsyncEventSourceResponse *> pending_sessions_;
Mutex pending_mutex_;
connect_handler_t on_connect_{};
esphome::web_server::WebServer *web_server_;
std::atomic<bool> has_pending_sessions_{false};
};
+1 -10
View File
@@ -542,16 +542,7 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
# An LN882H scan drops the association, so roaming costs a disconnect there.
cv.SplitDefault(
CONF_POST_CONNECT_ROAMING,
esp8266=True,
esp32=True,
rp2=True,
bk72xx=True,
rtl87xx=True,
ln882x=False,
): cv.boolean,
cv.Optional(CONF_POST_CONNECT_ROAMING, default=True): cv.boolean,
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(
single=True
+5 -6
View File
@@ -303,7 +303,7 @@ class WiFiAP {
bssid_t bssid_{}; // 6 bytes, all zeros = any/not set
uint8_t channel_{0}; // 1 byte, 0 = auto/not set
int8_t priority_{0}; // 1 byte
bool hidden_{false}; // 1 byte; WiFiAP is byte aligned unless manual IP or EAP adds a 4 byte aligned member
bool hidden_{false}; // 1 byte (+ 3 bytes end padding to 4-byte align)
};
class WiFiScanResult {
@@ -874,6 +874,9 @@ class WiFiComponent final : public Component {
#ifdef WIFI_SCAN_RESULTS_LOCK_ENABLED
Mutex scan_result_lock_;
#endif
#ifdef USE_WIFI_AP
WiFiAP ap_;
#endif
#ifdef USE_WIFI_IP_STATE_LISTENERS
StaticVector<WiFiIPStateListener *, ESPHOME_WIFI_IP_STATE_LISTENERS> ip_state_listeners_;
#endif
@@ -921,9 +924,6 @@ class WiFiComponent final : public Component {
uint32_t roaming_scan_end_{0}; // Timestamp when last roaming scan completed
#ifdef USE_WIFI_AP
uint32_t ap_timeout_{90000}; // Keep in sync with DEFAULT_AP_TIMEOUT in __init__.py
// WiFiAP is byte aligned unless manual IP or EAP is enabled; placed before the
// 1-byte members so they pack into its trailing bytes instead of padding after it
WiFiAP ap_;
#endif
// 1-byte enums and integers
@@ -983,8 +983,7 @@ class WiFiComponent final : public Component {
// Bools and bitfields
// Pending listener callbacks deferred from platform callbacks to main loop.
// Empty when no listener needs deferring (e.g. ESP32 without connect state listeners)
[[no_unique_address]] struct {
struct {
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
// Deferred until state machine reaches STA_CONNECTED so wifi.connected
// condition returns true in listener automations.
+9 -13
View File
@@ -27,7 +27,9 @@ namespace esphome {
template<class T, uint8_t SIZE> class FreeRTOSQueue {
public:
FreeRTOSQueue() : dropped_count_(0) { xQueueCreateStatic(SIZE, sizeof(T *), this->storage_, &this->queue_buf_); }
FreeRTOSQueue() : dropped_count_(0) {
this->handle_ = xQueueCreateStatic(SIZE, sizeof(T *), this->storage_, &this->queue_buf_);
}
// No destructor — ESPHome components are never destroyed. Intentionally
// omitted to avoid pulling in vQueueDelete code on resource-constrained targets.
@@ -42,7 +44,7 @@ template<class T, uint8_t SIZE> class FreeRTOSQueue {
if (element == nullptr)
return false;
if (xQueueSend(this->handle_(), &element, 0) != pdPASS) {
if (xQueueSend(this->handle_, &element, 0) != pdPASS) {
this->increment_dropped_count();
return false;
}
@@ -51,7 +53,7 @@ template<class T, uint8_t SIZE> class FreeRTOSQueue {
T *pop() {
T *element;
if (xQueueReceive(this->handle_(), &element, 0) != pdTRUE) {
if (xQueueReceive(this->handle_, &element, 0) != pdTRUE) {
return nullptr;
}
return element;
@@ -78,24 +80,18 @@ template<class T, uint8_t SIZE> class FreeRTOSQueue {
portEXIT_CRITICAL();
}
bool empty() const { return uxQueueMessagesWaiting(this->handle_()) == 0; }
bool empty() const { return uxQueueMessagesWaiting(this->handle_) == 0; }
bool full() const { return uxQueueSpacesAvailable(this->handle_()) == 0; }
bool full() const { return uxQueueSpacesAvailable(this->handle_) == 0; }
size_t size() const { return uxQueueMessagesWaiting(this->handle_()); }
size_t size() const { return uxQueueMessagesWaiting(this->handle_); }
protected:
// Static storage for the queue — lives in BSS, no heap allocation
uint8_t storage_[SIZE * sizeof(T *)];
StaticQueue_t queue_buf_;
QueueHandle_t handle_;
uint16_t dropped_count_;
// xQueueCreateStatic() returns the static queue structure itself as the handle;
// its only other checks are asserts, and a zero length is the one we could hit
static_assert(SIZE > 0, "FreeRTOSQueue needs at least one slot");
QueueHandle_t handle_() const {
return reinterpret_cast<QueueHandle_t>(const_cast<StaticQueue_t *>(&this->queue_buf_));
}
};
} // namespace esphome
+1 -2
View File
@@ -518,8 +518,7 @@ class Scheduler {
uint32_t hash_or_id,
SchedulerItem::Type type, bool find_first);
// Empty on single-threaded platforms (ESP8266, RP2), so it takes no space there
[[no_unique_address]] Mutex lock_;
Mutex lock_;
std::vector<SchedulerItem *> items_;
std::vector<SchedulerItem *> to_add_;
@@ -13,4 +13,3 @@ api:
port: 6054
reboot_timeout: 0s
batch_delay: 0ms
listen_backlog: 2
@@ -13,4 +13,3 @@ api:
port: 6053
reboot_timeout: 15min
batch_delay: 100ms
listen_backlog: 4
@@ -1,12 +0,0 @@
---
esphome:
name: test
esp8266:
board: d1_mini
wifi:
ssid: test
password: testtest
api:
@@ -12,7 +12,7 @@ def test_default_values_are_not_emitted(
component_config_path: Callable[[str], Path],
config_file: str,
) -> None:
"""Port 6053, a 15 min reboot timeout, 100 ms batch delay and backlog 4 are C++ initializers.
"""Port 6053, a 15 min reboot timeout and 100 ms batch delay are C++ initializers.
Both the schema defaults and the same values written explicitly take the skip path.
"""
@@ -21,7 +21,6 @@ def test_default_values_are_not_emitted(
assert "api_apiserver_id->set_port(" not in main_cpp
assert "api_apiserver_id->set_reboot_timeout(" not in main_cpp
assert "api_apiserver_id->set_batch_delay(" not in main_cpp
assert "api_apiserver_id->set_listen_backlog(" not in main_cpp
def test_custom_values_are_emitted(
@@ -34,14 +33,3 @@ def test_custom_values_are_emitted(
assert "api_apiserver_id->set_port(6054);" in main_cpp
assert "api_apiserver_id->set_reboot_timeout(0);" in main_cpp
assert "api_apiserver_id->set_batch_delay(0);" in main_cpp
assert "api_apiserver_id->set_listen_backlog(2);" in main_cpp
def test_esp8266_listen_backlog_is_emitted(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""The ESP8266 schema default of 1 differs from the C++ initializer, so it is set."""
main_cpp = generate_main(component_config_path("esp8266.yaml"))
assert "api_apiserver_id->set_listen_backlog(1);" in main_cpp
@@ -1,23 +0,0 @@
esphome:
name: test
esp8266:
board: esp01_1m
matrix_keypad:
- id: keypad_a
rows:
- pin: 12
- pin: 13
columns:
- pin: 14
- pin: 15
keys: "1234"
- id: keypad_b
rows:
- pin: 4
- pin: 5
columns:
- pin: 0
- pin: 2
keys: "1234"
@@ -1,27 +0,0 @@
"""Matrix keypad key codes live in a shared PROGMEM table instead of a std::string."""
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.components.matrix_keypad import check_keys
import esphome.config_validation as cv
def test_keys_use_shared_progmem_table(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("keys.yaml"))
assert (
"static constexpr uint8_t matrix_keypad_keys[] PROGMEM = {49, 50, 51, 52};"
in main_cpp
)
assert main_cpp.count("->set_keys(matrix_keypad_keys);") == 2
def test_non_ascii_keys_are_rejected() -> None:
with pytest.raises(cv.Invalid, match="'é' is not an ASCII"):
check_keys({"keys": "1é34"})
@@ -1,52 +0,0 @@
esphome:
name: test
esp8266:
board: esp01_1m
select:
- platform: template
id: mode
optimistic: true
options: [one, two, three]
button:
- platform: template
name: A
on_press:
- if:
condition:
select.is:
id: mode
options: [one, two]
then:
- logger.log: a
- platform: template
name: B
on_press:
- if:
condition:
select.is:
id: mode
options: [one, two]
then:
- logger.log: b
- platform: template
name: C
on_press:
- if:
condition:
select.is:
id: mode
options: three
then:
- logger.log: c
- if:
condition:
select.is:
id: mode
lambda: return current == "two";
then:
- logger.log: d
logger:
@@ -1,22 +0,0 @@
"""select.is option lists live in shared PROGMEM tables."""
from collections.abc import Callable
from pathlib import Path
import re
def test_select_is_options_use_shared_progmem_tables(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("select_is.yaml"))
tables = re.findall(
r"static constexpr const char \* (select_is_options\w*)\[\] PROGMEM = \{([^;]*)\};",
main_cpp,
)
# Two conditions with [one, two] share one table; [three] gets its own
assert sorted(body for _, body in tables) == ['"one", "two"', '"three"']
one_two = next(name for name, body in tables if body == '"one", "two"')
assert main_cpp.count(f", {one_two});") == 2
assert "SelectIsCondition<0" in main_cpp
@@ -1,12 +0,0 @@
---
esphome:
name: test
esp32:
board: esp32dev
uart:
- id: default_uart
tx_pin: GPIO17
rx_pin: GPIO16
baud_rate: 9600
@@ -1,13 +0,0 @@
---
esphome:
name: test
esp32:
board: esp32dev
uart:
- id: even_uart
tx_pin: GPIO17
rx_pin: GPIO16
baud_rate: 9600
parity: EVEN
-24
View File
@@ -1,24 +0,0 @@
"""Tests for UART parity code generation."""
from collections.abc import Callable
from pathlib import Path
def test_default_parity_is_not_emitted(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Parity NONE is the C++ initializer, so the setter is skipped."""
main_cpp = generate_main(component_config_path("parity_default.yaml"))
assert "default_uart->set_parity(" not in main_cpp
def test_custom_parity_is_emitted(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""A non default parity still reaches the setter."""
main_cpp = generate_main(component_config_path("parity_even.yaml"))
assert "even_uart->set_parity(uart::UART_CONFIG_PARITY_EVEN);" in main_cpp
@@ -1,16 +0,0 @@
---
esphome:
name: test
esp8266:
board: d1_mini
text_sensor:
- platform: version
id: default_version
name: Default Version
- platform: version
id: hidden_version
name: Hidden Version
hide_hash: true
hide_timestamp: true
@@ -1,17 +0,0 @@
"""Tests for the version text sensor codegen."""
from collections.abc import Callable
from pathlib import Path
def test_default_hide_flags_are_not_emitted(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Both flags are false in C++, so only true reaches the setters."""
main_cpp = generate_main(component_config_path("version.yaml"))
assert "default_version->set_hide_hash(" not in main_cpp
assert "default_version->set_hide_timestamp(" not in main_cpp
assert "hidden_version->set_hide_hash(true);" in main_cpp
assert "hidden_version->set_hide_timestamp(true);" in main_cpp
@@ -1,15 +0,0 @@
---
esphome:
name: test
esp32:
board: esp32dev
wifi:
ssid: test
password: testtest
web_server:
allowed_origins:
- https://app.esphome.io
- "*"
@@ -1,18 +0,0 @@
"""Tests for the web_server allowed_origins codegen."""
from collections.abc import Callable
from pathlib import Path
def test_allowed_origins_are_a_progmem_table(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Allowed origins live in a nullptr-terminated flash table instead of a heap vector."""
main_cpp = generate_main(component_config_path("allowed_origins.yaml"))
assert (
"static constexpr const char * web_server_allowed_origins[] PROGMEM = "
'{"https://app.esphome.io", "*", nullptr};' in main_cpp
)
assert "->set_allowed_origins(web_server_allowed_origins);" in main_cpp
@@ -1,12 +0,0 @@
---
esphome:
name: test
ln882x:
board: generic-ln882h
wifi:
ssid: test
password: testtest
ap:
ssid: fallback
@@ -23,7 +23,6 @@ def test_default_values_are_not_emitted(
assert "set_reboot_timeout(" not in main_cpp
assert "set_power_save_mode(" not in main_cpp
assert "set_min_auth_mode(" not in main_cpp
assert "set_post_connect_roaming(" not in main_cpp
def test_custom_values_are_emitted(
@@ -38,13 +37,3 @@ def test_custom_values_are_emitted(
assert "set_reboot_timeout(0);" in main_cpp
assert "set_power_save_mode(wifi::WIFI_POWER_SAVE_LIGHT);" in main_cpp
assert "set_min_auth_mode(wifi::WIFI_MIN_AUTH_MODE_WPA);" in main_cpp
def test_ln882x_skips_post_connect_roaming(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""LN882x defaults post_connect_roaming off."""
main_cpp = generate_main(component_config_path("bare_ln882x.yaml"))
assert "set_post_connect_roaming(false);" in main_cpp
-58
View File
@@ -33,20 +33,6 @@ button:
cycle: false
- select.first: select_test_select
- select.last: select_test_select
- if:
condition:
select.is:
id: select_test_select
options: [one, two]
then:
- select.next: select_test_select
- if:
condition:
select.is:
id: select_test_select
lambda: return current == "three";
then:
- select.first: select_test_select
- select.operation:
id: select_test_select
operation: next
@@ -55,47 +41,3 @@ button:
id: select_test_select
operation: !lambda return SELECT_OP_PREVIOUS;
cycle: !lambda return true;
- platform: template
name: "Test Select Options Lambdas"
on_press:
- lambda: |-
auto &options = id(select_test_select).traits.get_options();
for (size_t i = 0; i < options.size(); i++)
ESP_LOGD("test", "%s", options[i]);
- lambda: |-
std::string wanted = "two";
const auto &opts = id(select_test_select).traits.get_options();
if (opts.empty())
return;
for (int i = 0; i < (int) opts.size(); i++) {
if (opts[i] == wanted)
ESP_LOGD("test", "found at %d", i);
}
if (id(select_test_select).traits.get_options()[0] == wanted)
ESP_LOGD("test", "first");
auto it = std::find(opts.begin(), opts.end(), std::string("three"));
ESP_LOGD("test", "index %d", (int) (it - opts.begin()));
- lambda: |-
static std::vector<std::string> names{"red", "green"};
FixedVector<const char *> fresh;
fresh.init(names.size());
for (auto &name : names)
fresh.push_back(name.c_str());
if (id(select_test_select).traits.get_options().size() != fresh.size())
id(select_test_select).traits.set_options(fresh);
id(select_test_select).traits.set_options({"one", "two", "three"});
- lambda: |-
const char *value = "two";
const auto &options = id(select_test_select).traits.get_options();
const auto *it = std::find_if(options.begin(), options.end(),
[value](const char *option) { return strcmp(option, value) == 0; });
if (it == options.end())
return;
uint32_t index = (uint32_t) (it - options.begin());
ESP_LOGD("test", "index %u", (unsigned) index);
const auto *it2 = std::find(options.begin(), options.end(), options[0]);
ESP_LOGD("test", "first at %d", (int) (it2 - options.begin()));
- lambda: |-
auto &options = id(select_test_select).traits.get_options();
for (size_t i = 0; i < options.size(); i++)
ESP_LOGD("test", "%u: %s", (unsigned) i, options.at(i));
+3 -5
View File
@@ -5358,7 +5358,7 @@ def _setup_build_info_test(
return build_info_path, firmware_path
def test_compile_program_quietly_ignores_skip_bootloader_elsewhere(
def test_compile_program_warns_and_ignores_skip_bootloader_elsewhere(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A full build is always safe; orchestrators cannot see YAML overrides."""
@@ -5372,13 +5372,11 @@ def test_compile_program_quietly_ignores_skip_bootloader_elsewhere(
side_effect=EsphomeError("stop here"),
),
pytest.raises(EsphomeError, match="stop here"),
caplog.at_level("DEBUG"),
caplog.at_level("INFO"),
):
compile_program(MockArgs(), config)
assert [
r.levelno for r in caplog.records if "--skip-bootloader ignored" in r.message
] == [logging.DEBUG]
assert "--skip-bootloader ignored" in caplog.text
assert CORE.skip_bootloader is False