mirror of
https://github.com/esphome/esphome.git
synced 2026-02-01 09:17:34 -07:00
Compare commits
8 Commits
posix_tz
...
wifi_callb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c27835c03 | ||
|
|
0fd50b2381 | ||
|
|
9dcb469460 | ||
|
|
5e3561d60b | ||
|
|
ca9ed369f9 | ||
|
|
4e96b20b46 | ||
|
|
a1a60c44da | ||
|
|
898c8a5836 |
@@ -1 +1 @@
|
||||
cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab
|
||||
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3
|
||||
|
||||
@@ -1329,6 +1329,10 @@ async def to_code(config):
|
||||
# Disable dynamic log level control to save memory
|
||||
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
|
||||
|
||||
# Disable per-tag log level filtering since dynamic level control is disabled above
|
||||
# This saves ~250 bytes of RAM (tag cache) and associated code
|
||||
add_idf_sdkconfig_option("CONFIG_LOG_TAG_LEVEL_IMPL_NONE", True)
|
||||
|
||||
# Reduce PHY TX power in the event of a brownout
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
|
||||
|
||||
|
||||
@@ -11,12 +11,6 @@ namespace i2c {
|
||||
static const char *const TAG = "i2c";
|
||||
|
||||
void I2CBus::i2c_scan_() {
|
||||
// suppress logs from the IDF I2C library during the scan
|
||||
#if defined(USE_ESP32) && defined(USE_LOGGER)
|
||||
auto previous = esp_log_level_get("*");
|
||||
esp_log_level_set("*", ESP_LOG_NONE);
|
||||
#endif
|
||||
|
||||
for (uint8_t address = 8; address != 120; address++) {
|
||||
auto err = write_readv(address, nullptr, 0, nullptr, 0);
|
||||
if (err == ERROR_OK) {
|
||||
@@ -27,9 +21,6 @@ void I2CBus::i2c_scan_() {
|
||||
// it takes 16sec to scan on nrf52. It prevents board reset.
|
||||
arch_feed_wdt();
|
||||
}
|
||||
#if defined(USE_ESP32) && defined(USE_LOGGER)
|
||||
esp_log_level_set("*", previous);
|
||||
#endif
|
||||
}
|
||||
|
||||
ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len) {
|
||||
|
||||
@@ -114,9 +114,6 @@ void Logger::pre_setup() {
|
||||
|
||||
global_logger = this;
|
||||
esp_log_set_vprintf(esp_idf_log_vprintf_);
|
||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
|
||||
esp_log_level_set("*", ESP_LOG_VERBOSE);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,39 @@
|
||||
#include "mipi_spi.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace mipi_spi {} // namespace mipi_spi
|
||||
} // namespace esphome
|
||||
namespace esphome::mipi_spi {
|
||||
|
||||
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
|
||||
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
|
||||
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"MIPI_SPI Display\n"
|
||||
" Model: %s\n"
|
||||
" Width: %d\n"
|
||||
" Height: %d\n"
|
||||
" Swap X/Y: %s\n"
|
||||
" Mirror X: %s\n"
|
||||
" Mirror Y: %s\n"
|
||||
" Invert colors: %s\n"
|
||||
" Color order: %s\n"
|
||||
" Display pixels: %d bits\n"
|
||||
" Endianness: %s\n"
|
||||
" SPI Mode: %d\n"
|
||||
" SPI Data rate: %uMHz\n"
|
||||
" SPI Bus width: %d",
|
||||
model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)),
|
||||
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(invert_colors), (madctl & MADCTL_BGR) ? "BGR" : "RGB",
|
||||
display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast<unsigned>(data_rate / 1000000),
|
||||
bus_width);
|
||||
LOG_PIN(" CS Pin: ", cs);
|
||||
LOG_PIN(" Reset Pin: ", reset);
|
||||
LOG_PIN(" DC Pin: ", dc);
|
||||
if (offset_width != 0)
|
||||
ESP_LOGCONFIG(TAG, " Offset width: %d", offset_width);
|
||||
if (offset_height != 0)
|
||||
ESP_LOGCONFIG(TAG, " Offset height: %d", offset_height);
|
||||
if (brightness.has_value())
|
||||
ESP_LOGCONFIG(TAG, " Brightness: %u", brightness.value());
|
||||
}
|
||||
|
||||
} // namespace esphome::mipi_spi
|
||||
|
||||
@@ -63,6 +63,11 @@ enum BusType {
|
||||
BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer
|
||||
};
|
||||
|
||||
// Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro
|
||||
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
|
||||
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
|
||||
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width);
|
||||
|
||||
/**
|
||||
* Base class for MIPI SPI displays.
|
||||
* All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file.
|
||||
@@ -201,37 +206,9 @@ class MipiSpi : public display::Display,
|
||||
}
|
||||
|
||||
void dump_config() override {
|
||||
esph_log_config(TAG,
|
||||
"MIPI_SPI Display\n"
|
||||
" Model: %s\n"
|
||||
" Width: %u\n"
|
||||
" Height: %u",
|
||||
this->model_, WIDTH, HEIGHT);
|
||||
if constexpr (OFFSET_WIDTH != 0)
|
||||
esph_log_config(TAG, " Offset width: %u", OFFSET_WIDTH);
|
||||
if constexpr (OFFSET_HEIGHT != 0)
|
||||
esph_log_config(TAG, " Offset height: %u", OFFSET_HEIGHT);
|
||||
esph_log_config(TAG,
|
||||
" Swap X/Y: %s\n"
|
||||
" Mirror X: %s\n"
|
||||
" Mirror Y: %s\n"
|
||||
" Invert colors: %s\n"
|
||||
" Color order: %s\n"
|
||||
" Display pixels: %d bits\n"
|
||||
" Endianness: %s\n",
|
||||
YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)),
|
||||
YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_),
|
||||
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little");
|
||||
if (this->brightness_.has_value())
|
||||
esph_log_config(TAG, " Brightness: %u", this->brightness_.value());
|
||||
log_pin(TAG, " CS Pin: ", this->cs_);
|
||||
log_pin(TAG, " Reset Pin: ", this->reset_pin_);
|
||||
log_pin(TAG, " DC Pin: ", this->dc_pin_);
|
||||
esph_log_config(TAG,
|
||||
" SPI Mode: %d\n"
|
||||
" SPI Data rate: %dMHz\n"
|
||||
" SPI Bus width: %d",
|
||||
this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), BUS_TYPE);
|
||||
internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_,
|
||||
DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_,
|
||||
this->mode_, this->data_rate_, BUS_TYPE);
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -643,10 +643,34 @@ static bool topic_match(const char *message, const char *subscription) {
|
||||
}
|
||||
|
||||
void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) {
|
||||
for (auto &subscription : this->subscriptions_) {
|
||||
if (topic_match(topic.c_str(), subscription.topic.c_str()))
|
||||
subscription.callback(topic, payload);
|
||||
}
|
||||
#ifdef USE_ESP8266
|
||||
// IMPORTANT: This defer is REQUIRED to prevent stack overflow crashes on ESP8266.
|
||||
//
|
||||
// On ESP8266, this callback is invoked directly from the lwIP/AsyncTCP network stack
|
||||
// which runs in the "sys" context with a very limited stack (~4KB). By the time we
|
||||
// reach this function, the stack is already partially consumed by the network
|
||||
// processing chain: tcp_input -> AsyncClient::_recv -> AsyncMqttClient::_onMessage -> here.
|
||||
//
|
||||
// MQTT subscription callbacks can trigger arbitrary user actions (automations, HTTP
|
||||
// requests, sensor updates, etc.) which may have deep call stacks of their own.
|
||||
// For example, an HTTP request action requires: DNS lookup -> TCP connect -> TLS
|
||||
// handshake (if HTTPS) -> request formatting. This easily overflows the remaining
|
||||
// system stack space, causing a LoadStoreAlignmentCause exception or silent corruption.
|
||||
//
|
||||
// By deferring to the main loop, we ensure callbacks execute with a fresh, full-size
|
||||
// stack in the normal application context rather than the constrained network task.
|
||||
//
|
||||
// DO NOT REMOVE THIS DEFER without understanding the above. It may appear to work
|
||||
// in simple tests but will cause crashes with complex automations.
|
||||
this->defer([this, topic, payload]() {
|
||||
#endif
|
||||
for (auto &subscription : this->subscriptions_) {
|
||||
if (topic_match(topic.c_str(), subscription.topic.c_str()))
|
||||
subscription.callback(topic, payload);
|
||||
}
|
||||
#ifdef USE_ESP8266
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
// Setters
|
||||
|
||||
@@ -181,17 +181,20 @@ optional<bool> PMSX003Component::check_byte_() {
|
||||
bool PMSX003Component::check_payload_length_(uint16_t payload_length) {
|
||||
// https://avaldebe.github.io/PyPMS/sensors/Plantower/
|
||||
switch (this->type_) {
|
||||
case Type::PMSX003:
|
||||
// The expected payload length is typically 28 bytes.
|
||||
// However, a 20-byte payload check was already present in the code.
|
||||
// No official documentation was found confirming this.
|
||||
// Retaining this check to avoid breaking existing behavior.
|
||||
case Type::PMS1003:
|
||||
return payload_length == 28; // 2*13+2
|
||||
case Type::PMS3003: // Data 7/8/9 not set/reserved
|
||||
return payload_length == 20; // 2*9+2
|
||||
case Type::PMSX003: // Data 13 not set/reserved
|
||||
// Deprecated: Length 20 is for PMS3003 backwards compatibility
|
||||
return payload_length == 28 || payload_length == 20; // 2*13+2
|
||||
case Type::PMS5003S:
|
||||
case Type::PMS5003T:
|
||||
return payload_length == 28; // 2*13+2 (Data 13 not set/reserved)
|
||||
case Type::PMS5003ST:
|
||||
return payload_length == 36; // 2*17+2 (Data 16 not set/reserved)
|
||||
case Type::PMS5003T: // Data 13 not set/reserved
|
||||
return payload_length == 28; // 2*13+2
|
||||
case Type::PMS5003ST: // Data 16 not set/reserved
|
||||
return payload_length == 36; // 2*17+2
|
||||
case Type::PMS9003M:
|
||||
return payload_length == 28; // 2*13+2
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -314,9 +317,10 @@ void PMSX003Component::parse_data_() {
|
||||
}
|
||||
|
||||
// Firmware Version and Error Code
|
||||
if (this->type_ == Type::PMS5003ST) {
|
||||
const uint8_t firmware_version = this->data_[36];
|
||||
const uint8_t error_code = this->data_[37];
|
||||
if (this->type_ == Type::PMS1003 || this->type_ == Type::PMS5003ST || this->type_ == Type::PMS9003M) {
|
||||
const uint8_t firmware_error_code_offset = (this->type_ == Type::PMS5003ST) ? 36 : 28;
|
||||
const uint8_t firmware_version = this->data_[firmware_error_code_offset];
|
||||
const uint8_t error_code = this->data_[firmware_error_code_offset + 1];
|
||||
|
||||
ESP_LOGD(TAG, "Got Firmware Version: 0x%02X, Error Code: 0x%02X", firmware_version, error_code);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,13 @@
|
||||
namespace esphome::pmsx003 {
|
||||
|
||||
enum class Type : uint8_t {
|
||||
PMSX003 = 0,
|
||||
PMS1003 = 0,
|
||||
PMS3003,
|
||||
PMSX003, // PMS5003, PMS6003, PMS7003, PMSA003 (NOT PMSA003I - see `pmsa003i` component)
|
||||
PMS5003S,
|
||||
PMS5003T,
|
||||
PMS5003ST,
|
||||
PMS9003M,
|
||||
};
|
||||
|
||||
enum class Command : uint8_t {
|
||||
|
||||
@@ -40,33 +40,127 @@ pmsx003_ns = cg.esphome_ns.namespace("pmsx003")
|
||||
PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Component)
|
||||
PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor)
|
||||
|
||||
TYPE_PMSX003 = "PMSX003"
|
||||
TYPE_PMS1003 = "PMS1003"
|
||||
TYPE_PMS3003 = "PMS3003"
|
||||
TYPE_PMSX003 = "PMSX003" # PMS5003, PMS6003, PMS7003, PMSA003 (NOT PMSA003I - see `pmsa003i` component)
|
||||
TYPE_PMS5003S = "PMS5003S"
|
||||
TYPE_PMS5003T = "PMS5003T"
|
||||
TYPE_PMS5003ST = "PMS5003ST"
|
||||
TYPE_PMS9003M = "PMS9003M"
|
||||
|
||||
Type = pmsx003_ns.enum("Type", is_class=True)
|
||||
|
||||
PMSX003_TYPES = {
|
||||
TYPE_PMS1003: Type.PMS1003,
|
||||
TYPE_PMS3003: Type.PMS3003,
|
||||
TYPE_PMSX003: Type.PMSX003,
|
||||
TYPE_PMS5003S: Type.PMS5003S,
|
||||
TYPE_PMS5003T: Type.PMS5003T,
|
||||
TYPE_PMS5003ST: Type.PMS5003ST,
|
||||
TYPE_PMS9003M: Type.PMS9003M,
|
||||
}
|
||||
|
||||
SENSORS_TO_TYPE = {
|
||||
CONF_PM_1_0_STD: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_2_5_STD: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_10_0_STD: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_0_3UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_0_5UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_1_0UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_2_5UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_PM_5_0UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003ST],
|
||||
CONF_PM_10_0UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003ST],
|
||||
CONF_PM_1_0_STD: [
|
||||
TYPE_PMS1003,
|
||||
TYPE_PMS3003,
|
||||
TYPE_PMSX003,
|
||||
TYPE_PMS5003S,
|
||||
TYPE_PMS5003T,
|
||||
TYPE_PMS5003ST,
|
||||
TYPE_PMS9003M,
|
||||
],
|
||||
CONF_PM_2_5_STD: [
|
||||
TYPE_PMS1003,
|
||||
TYPE_PMS3003,
|
||||
TYPE_PMSX003,
|
||||
TYPE_PMS5003S,
|
||||
TYPE_PMS5003T,
|
||||
TYPE_PMS5003ST,
|
||||
TYPE_PMS9003M,
|
||||
],
|
||||
CONF_PM_10_0_STD: [
|
||||
TYPE_PMS1003,
|
||||
TYPE_PMS3003,
|
||||
TYPE_PMSX003,
|
||||
TYPE_PMS5003S,
|
||||
TYPE_PMS5003T,
|
||||
TYPE_PMS5003ST,
|
||||
TYPE_PMS9003M,
|
||||
],
|
||||
CONF_PM_1_0: [
|
||||
TYPE_PMS1003,
|
||||
TYPE_PMS3003,
|
||||
TYPE_PMSX003,
|
||||
TYPE_PMS5003S,
|
||||
TYPE_PMS5003T,
|
||||
TYPE_PMS5003ST,
|
||||
TYPE_PMS9003M,
|
||||
],
|
||||
CONF_PM_2_5: [
|
||||
TYPE_PMS1003,
|
||||
TYPE_PMS3003,
|
||||
TYPE_PMSX003,
|
||||
TYPE_PMS5003S,
|
||||
TYPE_PMS5003T,
|
||||
TYPE_PMS5003ST,
|
||||
TYPE_PMS9003M,
|
||||
],
|
||||
CONF_PM_10_0: [
|
||||
TYPE_PMS1003,
|
||||
TYPE_PMS3003,
|
||||
TYPE_PMSX003,
|
||||
TYPE_PMS5003S,
|
||||
TYPE_PMS5003T,
|
||||
TYPE_PMS5003ST,
|
||||
TYPE_PMS9003M,
|
||||
],
|
||||
CONF_PM_0_3UM: [
|
||||
TYPE_PMS1003,
|
||||
TYPE_PMSX003,
|
||||
TYPE_PMS5003S,
|
||||
TYPE_PMS5003T,
|
||||
TYPE_PMS5003ST,
|
||||
TYPE_PMS9003M,
|
||||
],
|
||||
CONF_PM_0_5UM: [
|
||||
TYPE_PMS1003,
|
||||
TYPE_PMSX003,
|
||||
TYPE_PMS5003S,
|
||||
TYPE_PMS5003T,
|
||||
TYPE_PMS5003ST,
|
||||
TYPE_PMS9003M,
|
||||
],
|
||||
CONF_PM_1_0UM: [
|
||||
TYPE_PMS1003,
|
||||
TYPE_PMSX003,
|
||||
TYPE_PMS5003S,
|
||||
TYPE_PMS5003T,
|
||||
TYPE_PMS5003ST,
|
||||
TYPE_PMS9003M,
|
||||
],
|
||||
CONF_PM_2_5UM: [
|
||||
TYPE_PMS1003,
|
||||
TYPE_PMSX003,
|
||||
TYPE_PMS5003S,
|
||||
TYPE_PMS5003T,
|
||||
TYPE_PMS5003ST,
|
||||
TYPE_PMS9003M,
|
||||
],
|
||||
CONF_PM_5_0UM: [
|
||||
TYPE_PMS1003,
|
||||
TYPE_PMSX003,
|
||||
TYPE_PMS5003S,
|
||||
TYPE_PMS5003ST,
|
||||
TYPE_PMS9003M,
|
||||
],
|
||||
CONF_PM_10_0UM: [
|
||||
TYPE_PMS1003,
|
||||
TYPE_PMSX003,
|
||||
TYPE_PMS5003S,
|
||||
TYPE_PMS5003ST,
|
||||
TYPE_PMS9003M,
|
||||
],
|
||||
CONF_FORMALDEHYDE: [TYPE_PMS5003S, TYPE_PMS5003ST],
|
||||
CONF_TEMPERATURE: [TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST],
|
||||
|
||||
@@ -12,8 +12,8 @@ from esphome.components.packet_transport import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
|
||||
from esphome.core import ID, Lambda
|
||||
from esphome.cpp_generator import ExpressionStatement, MockObj
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import literal
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DEPENDENCIES = ["network"]
|
||||
@@ -24,6 +24,8 @@ udp_ns = cg.esphome_ns.namespace("udp")
|
||||
UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
|
||||
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
|
||||
trigger_args = cg.std_vector.template(cg.uint8)
|
||||
trigger_argname = "data"
|
||||
trigger_argtype = [(trigger_args, trigger_argname)]
|
||||
|
||||
CONF_ADDRESSES = "addresses"
|
||||
CONF_LISTEN_ADDRESS = "listen_address"
|
||||
@@ -111,13 +113,14 @@ async def to_code(config):
|
||||
cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]]))
|
||||
if on_receive := config.get(CONF_ON_RECEIVE):
|
||||
on_receive = on_receive[0]
|
||||
trigger = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID])
|
||||
trigger_id = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID])
|
||||
trigger = await automation.build_automation(
|
||||
trigger, [(trigger_args, "data")], on_receive
|
||||
trigger_id, trigger_argtype, on_receive
|
||||
)
|
||||
trigger = Lambda(str(ExpressionStatement(trigger.trigger(MockObj("data")))))
|
||||
trigger = await cg.process_lambda(trigger, [(trigger_args, "data")])
|
||||
cg.add(var.add_listener(trigger))
|
||||
trigger_lambda = await cg.process_lambda(
|
||||
trigger.trigger(literal(trigger_argname)), trigger_argtype
|
||||
)
|
||||
cg.add(var.add_listener(trigger_lambda))
|
||||
cg.add(var.set_should_listen())
|
||||
|
||||
|
||||
|
||||
@@ -155,8 +155,9 @@ void USBCDCACMInstance::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a larger stack size for (very) verbose logging
|
||||
const size_t stack_size = esp_log_level_get(TAG) > ESP_LOG_DEBUG ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE;
|
||||
// Use a larger stack size for very verbose logging
|
||||
constexpr size_t stack_size =
|
||||
ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE;
|
||||
|
||||
// Create a simple, unique task name per interface
|
||||
char task_name[] = "usb_tx_0";
|
||||
|
||||
@@ -53,4 +53,4 @@ async def to_code(config):
|
||||
"lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"]
|
||||
)
|
||||
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.5")
|
||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
|
||||
@@ -643,7 +643,7 @@ void WiFiComponent::restart_adapter() {
|
||||
// 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;
|
||||
this->error_from_callback_ = 0;
|
||||
}
|
||||
|
||||
void WiFiComponent::loop() {
|
||||
@@ -1063,7 +1063,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
// 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;
|
||||
this->error_from_callback_ = 0;
|
||||
|
||||
if (!this->wifi_sta_connect_(ap)) {
|
||||
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
|
||||
@@ -1468,7 +1468,11 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
|
||||
}
|
||||
|
||||
if (this->error_from_callback_) {
|
||||
// ESP8266: logging done in callback, listeners deferred via pending_.disconnect
|
||||
// Other platforms: just log generic failure message
|
||||
#ifndef USE_ESP8266
|
||||
ESP_LOGW(TAG, "Connecting to network failed (callback)");
|
||||
#endif
|
||||
this->retry_connect();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,12 @@ static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127;
|
||||
/// Buffer size for SSID (IEEE 802.11 max 32 bytes + null terminator)
|
||||
static constexpr size_t SSID_BUFFER_SIZE = 33;
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
/// Special disconnect reason for authmode downgrade (CVE-2020-12638 mitigation)
|
||||
/// Not a real WiFi reason code - used internally for deferred logging
|
||||
static constexpr uint8_t WIFI_DISCONNECT_REASON_AUTHMODE_DOWNGRADE = 254;
|
||||
#endif
|
||||
|
||||
struct SavedWifiSettings {
|
||||
char ssid[33];
|
||||
char password[65];
|
||||
@@ -590,6 +596,9 @@ class WiFiComponent : public Component {
|
||||
void connect_soon_();
|
||||
|
||||
void wifi_loop_();
|
||||
#ifdef USE_ESP8266
|
||||
void process_pending_callbacks_();
|
||||
#endif
|
||||
bool wifi_mode_(optional<bool> sta, optional<bool> ap);
|
||||
bool wifi_sta_pre_setup_();
|
||||
bool wifi_apply_output_power_(float output_power);
|
||||
@@ -704,10 +713,26 @@ class WiFiComponent : public Component {
|
||||
uint8_t num_ipv6_addresses_{0};
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
// 0 = no error, non-zero = disconnect reason code from callback
|
||||
// This serves as both the error flag and stores the reason for deferred logging
|
||||
uint8_t error_from_callback_{0};
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// Pending listener callbacks from system context (ESP8266 only)
|
||||
// ESP8266 callbacks run in SDK system context with ~2KB stack where
|
||||
// calling arbitrary listener callbacks is unsafe. These flags defer
|
||||
// listener notifications to wifi_loop_() which runs with full stack.
|
||||
struct {
|
||||
bool connect : 1; // STA connected, notify listeners
|
||||
bool disconnect : 1; // STA disconnected, notify listeners
|
||||
bool got_ip : 1; // Got IP, notify listeners
|
||||
bool scan_complete : 1; // Scan complete, notify listeners
|
||||
} pending_{};
|
||||
#endif
|
||||
|
||||
// Group all boolean values together
|
||||
bool has_ap_{false};
|
||||
bool handled_connected_state_{false};
|
||||
bool error_from_callback_{false};
|
||||
bool scan_done_{false};
|
||||
bool ap_setup_{false};
|
||||
bool ap_started_{false};
|
||||
|
||||
@@ -511,21 +511,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
it.channel);
|
||||
#endif
|
||||
s_sta_connected = true;
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
for (auto *listener : global_wifi_component->connect_state_listeners_) {
|
||||
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)
|
||||
if (const WiFiAP *config = global_wifi_component->get_selected_sta_();
|
||||
config && config->get_manual_ip().has_value()) {
|
||||
for (auto *listener : global_wifi_component->ip_state_listeners_) {
|
||||
listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(),
|
||||
global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// Defer listener callbacks to main loop - system context has limited stack
|
||||
global_wifi_component->pending_.connect = true;
|
||||
break;
|
||||
}
|
||||
case EVENT_STAMODE_DISCONNECTED: {
|
||||
@@ -543,17 +530,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
}
|
||||
s_sta_connected = false;
|
||||
s_sta_connecting = false;
|
||||
// IMPORTANT: Set error flag BEFORE notifying listeners.
|
||||
// This ensures is_connected() returns false during listener callbacks,
|
||||
// which is critical for proper reconnection logic (e.g., roaming).
|
||||
global_wifi_component->error_from_callback_ = true;
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
// Notify listeners AFTER setting error flag so they see correct state
|
||||
static constexpr uint8_t EMPTY_BSSID[6] = {};
|
||||
for (auto *listener : global_wifi_component->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID);
|
||||
}
|
||||
#endif
|
||||
// Store reason as error flag; defer listener callbacks to main loop
|
||||
global_wifi_component->error_from_callback_ = it.reason;
|
||||
global_wifi_component->pending_.disconnect = true;
|
||||
break;
|
||||
}
|
||||
case EVENT_STAMODE_AUTHMODE_CHANGE: {
|
||||
@@ -564,10 +543,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
// https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
|
||||
if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) {
|
||||
ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
|
||||
// we can't call retry_connect() from this context, so disconnect immediately
|
||||
// and notify main thread with error_from_callback_
|
||||
wifi_station_disconnect();
|
||||
global_wifi_component->error_from_callback_ = true;
|
||||
global_wifi_component->error_from_callback_ = WIFI_DISCONNECT_REASON_AUTHMODE_DOWNGRADE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -578,12 +555,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", network::IPAddress(&it.ip).str_to(ip_buf),
|
||||
network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf));
|
||||
s_sta_got_ip = true;
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
for (auto *listener : global_wifi_component->ip_state_listeners_) {
|
||||
listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), global_wifi_component->get_dns_address(0),
|
||||
global_wifi_component->get_dns_address(1));
|
||||
}
|
||||
#endif
|
||||
// Defer listener callbacks to main loop - system context has limited stack
|
||||
global_wifi_component->pending_.got_ip = true;
|
||||
break;
|
||||
}
|
||||
case EVENT_STAMODE_DHCP_TIMEOUT: {
|
||||
@@ -793,11 +766,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
||||
ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(),
|
||||
needs_full ? "" : " (filtered)");
|
||||
this->scan_done_ = true;
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : global_wifi_component->scan_results_listeners_) {
|
||||
listener->on_wifi_scan_results(global_wifi_component->scan_result_);
|
||||
}
|
||||
#endif
|
||||
this->pending_.scan_complete = true; // Defer listener callbacks to main loop
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
@@ -983,7 +952,59 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() {
|
||||
return network::IPAddress(&ip.gw);
|
||||
}
|
||||
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); }
|
||||
void WiFiComponent::wifi_loop_() {}
|
||||
void WiFiComponent::wifi_loop_() { this->process_pending_callbacks_(); }
|
||||
|
||||
void WiFiComponent::process_pending_callbacks_() {
|
||||
// Notify listeners for connect event (logging already done in callback)
|
||||
if (this->pending_.connect) {
|
||||
this->pending_.connect = false;
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
bssid_t bssid = this->wifi_bssid();
|
||||
char ssid_buf[SSID_BUFFER_SIZE];
|
||||
for (auto *listener : this->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(StringRef(this->wifi_ssid_to(ssid_buf)), bssid);
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
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
|
||||
}
|
||||
|
||||
// Notify listeners for disconnect event (logging already done in callback)
|
||||
if (this->pending_.disconnect) {
|
||||
this->pending_.disconnect = false;
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
static constexpr uint8_t EMPTY_BSSID[6] = {};
|
||||
for (auto *listener : this->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Notify listeners for got IP event (logging already done in callback)
|
||||
if (this->pending_.got_ip) {
|
||||
this->pending_.got_ip = false;
|
||||
#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
|
||||
}
|
||||
|
||||
// Notify listeners for scan complete (logging already done in callback)
|
||||
if (this->pending_.scan_complete) {
|
||||
this->pending_.scan_complete = false;
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : this->scan_results_listeners_) {
|
||||
listener->on_wifi_scan_results(this->scan_result_);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::wifi
|
||||
#endif
|
||||
|
||||
@@ -774,7 +774,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
}
|
||||
s_sta_connected = false;
|
||||
s_sta_connecting = false;
|
||||
error_from_callback_ = true;
|
||||
error_from_callback_ = 1;
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
static constexpr uint8_t EMPTY_BSSID[6] = {};
|
||||
for (auto *listener : this->connect_state_listeners_) {
|
||||
|
||||
@@ -494,7 +494,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
s_ignored_disconnect_count, get_disconnect_reason_str(it.reason));
|
||||
s_sta_state = LTWiFiSTAState::ERROR_FAILED;
|
||||
WiFi.disconnect();
|
||||
this->error_from_callback_ = true;
|
||||
this->error_from_callback_ = 1;
|
||||
// Don't break - fall through to notify listeners
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s, count=%u)",
|
||||
@@ -520,7 +520,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL ||
|
||||
reason == WIFI_REASON_HANDSHAKE_TIMEOUT) {
|
||||
WiFi.disconnect();
|
||||
this->error_from_callback_ = true;
|
||||
this->error_from_callback_ = 1;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
@@ -539,7 +539,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) {
|
||||
ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
|
||||
WiFi.disconnect();
|
||||
this->error_from_callback_ = true;
|
||||
this->error_from_callback_ = 1;
|
||||
s_sta_state = LTWiFiSTAState::ERROR_FAILED;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -278,9 +278,13 @@ LAMBDA_PROG = re.compile(r"\bid\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)")
|
||||
|
||||
class Lambda:
|
||||
def __init__(self, value):
|
||||
from esphome.cpp_generator import Expression, statement
|
||||
|
||||
# pylint: disable=protected-access
|
||||
if isinstance(value, Lambda):
|
||||
self._value = value._value
|
||||
elif isinstance(value, Expression):
|
||||
self._value = str(statement(value))
|
||||
else:
|
||||
self._value = value
|
||||
self._parts = None
|
||||
|
||||
@@ -210,7 +210,7 @@ void Application::loop() {
|
||||
#ifdef USE_ESP32
|
||||
esp_chip_info_t chip_info;
|
||||
esp_chip_info(&chip_info);
|
||||
ESP_LOGI(TAG, "ESP32 Chip: %s r%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100,
|
||||
ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100,
|
||||
chip_info.revision % 100, chip_info.cores);
|
||||
#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET)
|
||||
// Suggest optimization for chips that don't need the PSRAM cache workaround
|
||||
|
||||
@@ -462,6 +462,16 @@ def statement(expression: Expression | Statement) -> Statement:
|
||||
return ExpressionStatement(expression)
|
||||
|
||||
|
||||
def literal(name: str) -> "MockObj":
|
||||
"""Create a literal name that will appear in the generated code
|
||||
not surrounded by quotes.
|
||||
|
||||
:param name: The name of the literal.
|
||||
:return: The literal as a MockObj.
|
||||
"""
|
||||
return MockObj(name, "")
|
||||
|
||||
|
||||
def variable(
|
||||
id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True
|
||||
) -> "MockObj":
|
||||
@@ -665,7 +675,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
|
||||
|
||||
|
||||
async def process_lambda(
|
||||
value: Lambda,
|
||||
value: Lambda | Expression,
|
||||
parameters: TemplateArgsType,
|
||||
capture: str = "",
|
||||
return_type: SafeExpType = None,
|
||||
@@ -689,6 +699,14 @@ async def process_lambda(
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
# Inadvertently passing a malformed parameters value will lead to the build process mysteriously hanging at the
|
||||
# "Generating C++ source..." stage, so check here to save the developer's hair.
|
||||
assert isinstance(parameters, list) and all(
|
||||
isinstance(p, tuple) and len(p) == 2 for p in parameters
|
||||
)
|
||||
if isinstance(value, Expression):
|
||||
value = Lambda(value)
|
||||
|
||||
parts = value.parts[:]
|
||||
for i, id in enumerate(value.requires_ids):
|
||||
full_id, var = await get_variable_with_full_id(id)
|
||||
|
||||
@@ -114,7 +114,7 @@ lib_deps =
|
||||
ESP8266WiFi ; wifi (Arduino built-in)
|
||||
Update ; ota (Arduino built-in)
|
||||
ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp
|
||||
ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base
|
||||
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
|
||||
makuna/NeoPixelBus@2.7.3 ; neopixelbus
|
||||
ESP8266HTTPClient ; http_request (Arduino built-in)
|
||||
ESP8266mDNS ; mdns (Arduino built-in)
|
||||
@@ -202,7 +202,7 @@ lib_deps =
|
||||
${common:arduino.lib_deps}
|
||||
ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp
|
||||
bblanchon/ArduinoJson@7.4.2 ; json
|
||||
ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base
|
||||
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
|
||||
build_flags =
|
||||
${common:arduino.build_flags}
|
||||
-DUSE_RP2040
|
||||
@@ -218,7 +218,7 @@ framework = arduino
|
||||
lib_compat_mode = soft
|
||||
lib_deps =
|
||||
bblanchon/ArduinoJson@7.4.2 ; json
|
||||
ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base
|
||||
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
|
||||
droscy/esp_wireguard@0.4.2 ; wireguard
|
||||
build_flags =
|
||||
${common:arduino.build_flags}
|
||||
|
||||
10
tests/components/mipi_spi/test.esp8266-ard.yaml
Normal file
10
tests/components/mipi_spi/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
substitutions:
|
||||
dc_pin: GPIO15
|
||||
cs_pin: GPIO5
|
||||
enable_pin: GPIO4
|
||||
reset_pin: GPIO16
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -347,3 +347,280 @@ class TestMockObj:
|
||||
assert isinstance(actual, cg.MockObj)
|
||||
assert actual.base == "foo.eek"
|
||||
assert actual.op == "."
|
||||
|
||||
|
||||
class TestStatementFunction:
|
||||
"""Tests for the statement() function."""
|
||||
|
||||
def test_statement__expression_converted_to_statement(self):
|
||||
"""Test that expressions are converted to ExpressionStatement."""
|
||||
expr = cg.RawExpression("foo()")
|
||||
result = cg.statement(expr)
|
||||
|
||||
assert isinstance(result, cg.ExpressionStatement)
|
||||
assert str(result) == "foo();"
|
||||
|
||||
def test_statement__statement_unchanged(self):
|
||||
"""Test that statements are returned unchanged."""
|
||||
stmt = cg.RawStatement("foo()")
|
||||
result = cg.statement(stmt)
|
||||
|
||||
assert result is stmt
|
||||
assert str(result) == "foo()"
|
||||
|
||||
def test_statement__expression_statement_unchanged(self):
|
||||
"""Test that ExpressionStatement is returned unchanged."""
|
||||
stmt = cg.ExpressionStatement(42)
|
||||
result = cg.statement(stmt)
|
||||
|
||||
assert result is stmt
|
||||
assert str(result) == "42;"
|
||||
|
||||
def test_statement__line_comment_unchanged(self):
|
||||
"""Test that LineComment is returned unchanged."""
|
||||
stmt = cg.LineComment("This is a comment")
|
||||
result = cg.statement(stmt)
|
||||
|
||||
assert result is stmt
|
||||
assert str(result) == "// This is a comment"
|
||||
|
||||
|
||||
class TestLiteralFunction:
|
||||
"""Tests for the literal() function."""
|
||||
|
||||
def test_literal__creates_mockobj(self):
|
||||
"""Test that literal() creates a MockObj."""
|
||||
result = cg.literal("MY_CONSTANT")
|
||||
|
||||
assert isinstance(result, cg.MockObj)
|
||||
assert result.base == "MY_CONSTANT"
|
||||
assert result.op == ""
|
||||
|
||||
def test_literal__string_representation(self):
|
||||
"""Test that literal names appear unquoted in generated code."""
|
||||
result = cg.literal("nullptr")
|
||||
|
||||
assert str(result) == "nullptr"
|
||||
|
||||
def test_literal__can_be_used_in_expressions(self):
|
||||
"""Test that literals can be used as part of larger expressions."""
|
||||
null_lit = cg.literal("nullptr")
|
||||
expr = cg.CallExpression(cg.RawExpression("my_func"), null_lit)
|
||||
|
||||
assert str(expr) == "my_func(nullptr)"
|
||||
|
||||
def test_literal__common_cpp_literals(self):
|
||||
"""Test common C++ literal values."""
|
||||
test_cases = [
|
||||
("nullptr", "nullptr"),
|
||||
("true", "true"),
|
||||
("false", "false"),
|
||||
("NULL", "NULL"),
|
||||
("NAN", "NAN"),
|
||||
]
|
||||
|
||||
for name, expected in test_cases:
|
||||
result = cg.literal(name)
|
||||
assert str(result) == expected
|
||||
|
||||
|
||||
class TestLambdaConstructor:
|
||||
"""Tests for the Lambda class constructor in core/__init__.py."""
|
||||
|
||||
def test_lambda__from_string(self):
|
||||
"""Test Lambda constructor with string argument."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
lambda_obj = Lambda("return x + 1;")
|
||||
|
||||
assert lambda_obj.value == "return x + 1;"
|
||||
assert str(lambda_obj) == "return x + 1;"
|
||||
|
||||
def test_lambda__from_expression(self):
|
||||
"""Test Lambda constructor with Expression argument."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
expr = cg.RawExpression("x + 1")
|
||||
lambda_obj = Lambda(expr)
|
||||
|
||||
# Expression should be converted to statement (with semicolon)
|
||||
assert lambda_obj.value == "x + 1;"
|
||||
|
||||
def test_lambda__from_lambda(self):
|
||||
"""Test Lambda constructor with another Lambda argument."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
original = Lambda("return x + 1;")
|
||||
copy = Lambda(original)
|
||||
|
||||
assert copy.value == original.value
|
||||
assert copy.value == "return x + 1;"
|
||||
|
||||
def test_lambda__parts_parsing(self):
|
||||
"""Test that Lambda correctly parses parts with id() references."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
lambda_obj = Lambda("return id(my_sensor).state;")
|
||||
parts = lambda_obj.parts
|
||||
|
||||
# Parts should be split by LAMBDA_PROG regex: text, id, op, text
|
||||
assert len(parts) == 4
|
||||
assert parts[0] == "return "
|
||||
assert parts[1] == "my_sensor"
|
||||
assert parts[2] == "."
|
||||
assert parts[3] == "state;"
|
||||
|
||||
def test_lambda__requires_ids(self):
|
||||
"""Test that Lambda correctly extracts required IDs."""
|
||||
from esphome.core import ID, Lambda
|
||||
|
||||
lambda_obj = Lambda("return id(sensor1).state + id(sensor2).value;")
|
||||
ids = lambda_obj.requires_ids
|
||||
|
||||
assert len(ids) == 2
|
||||
assert all(isinstance(id_obj, ID) for id_obj in ids)
|
||||
assert ids[0].id == "sensor1"
|
||||
assert ids[1].id == "sensor2"
|
||||
|
||||
def test_lambda__no_ids(self):
|
||||
"""Test Lambda with no id() references."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
lambda_obj = Lambda("return 42;")
|
||||
ids = lambda_obj.requires_ids
|
||||
|
||||
assert len(ids) == 0
|
||||
|
||||
def test_lambda__comment_removal(self):
|
||||
"""Test that comments are removed when parsing parts."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
lambda_obj = Lambda("return id(sensor).state; // Get sensor state")
|
||||
parts = lambda_obj.parts
|
||||
|
||||
# Comment should be replaced with space, not affect parsing
|
||||
assert "my_sensor" not in str(parts)
|
||||
|
||||
def test_lambda__multiline_string(self):
|
||||
"""Test Lambda with multiline string."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
code = """if (id(sensor).state > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;"""
|
||||
lambda_obj = Lambda(code)
|
||||
|
||||
assert lambda_obj.value == code
|
||||
assert "sensor" in [id_obj.id for id_obj in lambda_obj.requires_ids]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestProcessLambda:
|
||||
"""Tests for the process_lambda() async function."""
|
||||
|
||||
async def test_process_lambda__none_value(self):
|
||||
"""Test that None returns None."""
|
||||
result = await cg.process_lambda(None, [])
|
||||
|
||||
assert result is None
|
||||
|
||||
async def test_process_lambda__with_expression(self):
|
||||
"""Test process_lambda with Expression argument."""
|
||||
|
||||
expr = cg.RawExpression("return x + 1")
|
||||
result = await cg.process_lambda(expr, [(int, "x")])
|
||||
|
||||
assert isinstance(result, cg.LambdaExpression)
|
||||
assert "x + 1" in str(result)
|
||||
|
||||
async def test_process_lambda__simple_lambda_no_ids(self):
|
||||
"""Test process_lambda with simple Lambda without id() references."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
lambda_obj = Lambda("return x + 1;")
|
||||
result = await cg.process_lambda(lambda_obj, [(int, "x")])
|
||||
|
||||
assert isinstance(result, cg.LambdaExpression)
|
||||
# Should have parameter
|
||||
lambda_str = str(result)
|
||||
assert "int32_t x" in lambda_str
|
||||
assert "return x + 1;" in lambda_str
|
||||
|
||||
async def test_process_lambda__with_return_type(self):
|
||||
"""Test process_lambda with return type specified."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
lambda_obj = Lambda("return x > 0;")
|
||||
result = await cg.process_lambda(lambda_obj, [(int, "x")], return_type=bool)
|
||||
|
||||
assert isinstance(result, cg.LambdaExpression)
|
||||
lambda_str = str(result)
|
||||
assert "-> bool" in lambda_str
|
||||
|
||||
async def test_process_lambda__with_capture(self):
|
||||
"""Test process_lambda with capture specified."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
lambda_obj = Lambda("return captured + x;")
|
||||
result = await cg.process_lambda(lambda_obj, [(int, "x")], capture="captured")
|
||||
|
||||
assert isinstance(result, cg.LambdaExpression)
|
||||
lambda_str = str(result)
|
||||
assert "[captured]" in lambda_str
|
||||
|
||||
async def test_process_lambda__empty_capture(self):
|
||||
"""Test process_lambda with empty capture (stateless lambda)."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
lambda_obj = Lambda("return x + 1;")
|
||||
result = await cg.process_lambda(lambda_obj, [(int, "x")], capture="")
|
||||
|
||||
assert isinstance(result, cg.LambdaExpression)
|
||||
lambda_str = str(result)
|
||||
assert "[]" in lambda_str
|
||||
|
||||
async def test_process_lambda__no_parameters(self):
|
||||
"""Test process_lambda with no parameters."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
lambda_obj = Lambda("return 42;")
|
||||
result = await cg.process_lambda(lambda_obj, [])
|
||||
|
||||
assert isinstance(result, cg.LambdaExpression)
|
||||
lambda_str = str(result)
|
||||
# Should have empty parameter list
|
||||
assert "()" in lambda_str
|
||||
|
||||
async def test_process_lambda__multiple_parameters(self):
|
||||
"""Test process_lambda with multiple parameters."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
lambda_obj = Lambda("return x + y + z;")
|
||||
result = await cg.process_lambda(
|
||||
lambda_obj, [(int, "x"), (float, "y"), (bool, "z")]
|
||||
)
|
||||
|
||||
assert isinstance(result, cg.LambdaExpression)
|
||||
lambda_str = str(result)
|
||||
assert "int32_t x" in lambda_str
|
||||
assert "float y" in lambda_str
|
||||
assert "bool z" in lambda_str
|
||||
|
||||
async def test_process_lambda__parameter_validation(self):
|
||||
"""Test that malformed parameters raise assertion error."""
|
||||
from esphome.core import Lambda
|
||||
|
||||
lambda_obj = Lambda("return x;")
|
||||
|
||||
# Test invalid parameter format (not list of tuples)
|
||||
with pytest.raises(AssertionError):
|
||||
await cg.process_lambda(lambda_obj, "invalid")
|
||||
|
||||
# Test invalid tuple format (not 2-element tuples)
|
||||
with pytest.raises(AssertionError):
|
||||
await cg.process_lambda(lambda_obj, [(int, "x", "extra")])
|
||||
|
||||
# Test invalid tuple format (single element)
|
||||
with pytest.raises(AssertionError):
|
||||
await cg.process_lambda(lambda_obj, [(int,)])
|
||||
|
||||
Reference in New Issue
Block a user