Compare commits

...

25 Commits

Author SHA1 Message Date
J. Nick Koston
a5253d45b0 tweak comments 2026-01-31 01:45:01 -06:00
J. Nick Koston
385750d8c3 tweak comments 2026-01-31 01:44:49 -06:00
J. Nick Koston
a8852e9d7d tweak comments 2026-01-31 01:44:39 -06:00
J. Nick Koston
3ebe6a38b1 fixes 2026-01-31 01:31:29 -06:00
J. Nick Koston
7c017f4075 [esp32] Exclude additional unused IDF components (driver, dac, mcpwm, openthread, ulp) 2026-01-31 00:52:54 -06:00
Clyde Stubbs
9dcb469460 [core] Simplify generation of Lambda during to_code() (#13533) 2026-01-31 12:18:30 +11:00
J0k3r2k1
5e3561d60b [mipi_spi] Fix log_pin() FlashStringHelper compatibility (#13624)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-01-30 14:33:45 -06:00
Thomas Rupprecht
ca9ed369f9 [pmsx003] support device-types PMS1003, PMS3003, PMS9003M (#13640) 2026-01-30 14:59:47 -05:00
J. Nick Koston
4e96b20b46 [mqtt] Restore ESP8266 on_message defer to prevent stack overflow (#13648) 2026-01-30 12:49:14 -06:00
J. Nick Koston
a1a60c44da [web_server_base] Update ESPAsyncWebServer to 3.9.6 (#13639) 2026-01-30 12:48:34 -06:00
Shivam Maurya
898c8a5836 [core] ESP32 chip revision text (#13647) 2026-01-30 11:01:00 -05:00
Thomas Rupprecht
20edd11ca7 [pmsx003] Improvements (#13626) 2026-01-29 22:48:16 -05:00
J. Nick Koston
9a8c71a58b [logger] Fix USB Serial JTAG VFS linker errors when using UART on IDF (#13628)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 21:31:01 -06:00
Jonathan Swoboda
1a7435250e Merge branch 'release' into dev 2026-01-29 22:22:23 -05:00
Jonathan Swoboda
3c91d72403 Merge pull request #13632 from esphome/bump-2026.1.3
2026.1.3
2026-01-29 22:22:10 -05:00
Jonathan Swoboda
0a63fc6f05 Bump version to 2026.1.3 2026-01-29 21:11:09 -05:00
J. Nick Koston
50e739ee8e [http_request] Fix empty body for chunked transfer encoding responses (#13599) 2026-01-29 21:11:09 -05:00
J. Nick Koston
6c84f20491 [wifi] Fix ESP8266 yield panic when WiFi scan fails (#13603) 2026-01-29 21:11:09 -05:00
Cody Cutrer
a68506f924 [ld2450] preserve precision of angle (#13600) 2026-01-29 21:11:08 -05:00
esphomebot
a20d42ca0b Update webserver local assets to 20260127-190637 (#13573)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-29 21:11:08 -05:00
J. Nick Koston
4ec8846198 [web_server] Add name_id to SSE for entity ID format migration (#13535) 2026-01-29 21:11:08 -05:00
J. Nick Koston
40ea65b1c0 [socket] ESP8266: call delay(0) instead of esp_delay(0, cb) for zero timeout (#13530) 2026-01-29 21:11:08 -05:00
J. Nick Koston
f7937ef952 [ota] Improve error message when device closes connection without responding (#13562) 2026-01-29 21:11:08 -05:00
sebcaps
d6bf137026 [mhz19] Fix Uninitialized var warning message (#13526) 2026-01-29 21:11:08 -05:00
esphomebot
ed9a672f44 Update webserver local assets to 20260122-204614 (#13455)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-29 21:11:08 -05:00
22 changed files with 644 additions and 153 deletions

View File

@@ -1 +1 @@
cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3

View File

@@ -124,10 +124,14 @@ COMPILER_OPTIMIZATIONS = {
# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain
DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
"driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers
"esp_adc", # ADC driver - only needed by adc component
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
"esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM
"esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
"esp_eth", # Ethernet driver - only needed by ethernet component
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
"esp_http_client", # HTTP client - only needed by http_request component
@@ -138,9 +142,11 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"espcoredump", # Core dump support - ESPHome has its own debug component
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
"mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation
"openthread", # Thread protocol - only needed by openthread component
"perfmon", # Xtensa performance monitor - ESPHome has its own debug component
"protocomm", # Protocol communication for provisioning - unused by ESPHome
"spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only)
"ulp", # ULP coprocessor - not currently used by any ESPHome component
"unity", # Unit testing framework - ESPHome doesn't use IDF's testing
"wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused
"wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation
@@ -739,9 +745,10 @@ CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
CONF_DISABLE_FATFS = "disable_fatfs"
# VFS requirement tracking
# Components that need VFS features can call require_vfs_select() or require_vfs_dir()
# Components that need VFS features can call require_vfs_*() functions
KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required"
# Feature requirement tracking - components can call require_* functions to re-enable
# These are stored in CORE.data[KEY_ESP32] dict
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
@@ -768,6 +775,15 @@ def require_vfs_dir() -> None:
CORE.data[KEY_VFS_DIR_REQUIRED] = True
def require_vfs_termios() -> None:
"""Mark that VFS termios support is required by a component.
Call this from components that use terminal I/O functions (usb_serial_jtag_vfs_*, etc.).
This prevents CONFIG_VFS_SUPPORT_TERMIOS from being disabled.
"""
CORE.data[KEY_VFS_TERMIOS_REQUIRED] = True
def require_full_certificate_bundle() -> None:
"""Request the full certificate bundle instead of the common-CAs-only bundle.
@@ -1372,11 +1388,18 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
# Disable VFS support for termios (terminal I/O functions)
# ESPHome doesn't use termios functions on ESP32 (only used in host UART driver).
# USB Serial JTAG VFS functions require termios support.
# Components that need it (e.g., logger when USB_SERIAL_JTAG is supported but not selected
# as the logger output) call require_vfs_termios().
# Saves approximately 1.8KB of flash when disabled (default).
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS]
)
if CORE.data.get(KEY_VFS_TERMIOS_REQUIRED, False):
# Component requires VFS termios - force enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_TERMIOS", True)
else:
# No component needs it - allow user to control (default: disabled)
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS]
)
# Disable VFS support for select() with file descriptors
# ESPHome only uses select() with sockets via lwip_select(), which still works.

View File

@@ -15,6 +15,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32S2,
VARIANT_ESP32S3,
get_esp32_variant,
include_builtin_idf_component,
)
import esphome.config_validation as cv
from esphome.const import (
@@ -121,6 +122,10 @@ def get_default_tx_enqueue_timeout(bit_rate):
async def to_code(config):
# Legacy driver component provides driver/twai.h header
include_builtin_idf_component("driver")
# Also enable esp_driver_twai for future migration to new API
include_builtin_idf_component("esp_driver_twai")
var = cg.new_Pvariable(config[CONF_ID])
await canbus.register_canbus(var, config)

View File

@@ -1,7 +1,12 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import output
from esphome.components.esp32 import VARIANT_ESP32, VARIANT_ESP32S2, get_esp32_variant
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32S2,
get_esp32_variant,
include_builtin_idf_component,
)
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN
@@ -38,6 +43,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
async def to_code(config):
include_builtin_idf_component("esp_driver_dac")
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await output.register_output(var, config)

View File

@@ -269,6 +269,8 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
# Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_touch_sens")
# Legacy driver component provides driver/touch_sensor.h header
include_builtin_idf_component("driver")
touch = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(touch, config)

View File

@@ -16,6 +16,8 @@ from esphome.components.esp32 import (
VARIANT_ESP32S3,
add_idf_sdkconfig_option,
get_esp32_variant,
require_usb_serial_jtag_secondary,
require_vfs_termios,
)
from esphome.components.libretiny import get_libretiny_component, get_libretiny_family
from esphome.components.libretiny.const import (
@@ -397,9 +399,15 @@ async def to_code(config):
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG")
# Define platform support flags for components that need auto-detection
try:
uart_selection(USB_SERIAL_JTAG)
cg.add_define("USE_LOGGER_USB_SERIAL_JTAG")
# USB Serial JTAG code is compiled when platform supports it.
# Enable secondary USB serial JTAG console so the VFS functions are available.
if CORE.is_esp32 and config[CONF_HARDWARE_UART] != USB_SERIAL_JTAG:
require_usb_serial_jtag_secondary()
require_vfs_termios()
except cv.Invalid:
pass
try:

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32C6,
VARIANT_ESP32H2,
add_idf_sdkconfig_option,
include_builtin_idf_component,
only_on_variant,
require_vfs_select,
)
@@ -172,6 +173,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
# Re-enable openthread IDF component (excluded by default)
include_builtin_idf_component("openthread")
cg.add_define("USE_OPENTHREAD")
# OpenThread SRP needs access to mDNS services after setup

View File

@@ -2,21 +2,20 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace pmsx003 {
namespace esphome::pmsx003 {
static const char *const TAG = "pmsx003";
static const uint8_t START_CHARACTER_1 = 0x42;
static const uint8_t START_CHARACTER_2 = 0x4D;
static const uint16_t PMS_STABILISING_MS = 30000; // time taken for the sensor to become stable after power on in ms
static const uint16_t STABILISING_MS = 30000; // time taken for the sensor to become stable after power on in ms
static const uint16_t PMS_CMD_MEASUREMENT_MODE_PASSIVE =
0x0000; // use `PMS_CMD_MANUAL_MEASUREMENT` to trigger a measurement
static const uint16_t PMS_CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automatically perform measurements
static const uint16_t PMS_CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode
static const uint16_t PMS_CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode
static const uint16_t CMD_MEASUREMENT_MODE_PASSIVE =
0x0000; // use `Command::MANUAL_MEASUREMENT` to trigger a measurement
static const uint16_t CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automatically perform measurements
static const uint16_t CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode
static const uint16_t CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode
void PMSX003Component::setup() {}
@@ -42,7 +41,7 @@ void PMSX003Component::dump_config() {
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
if (this->update_interval_ <= PMS_STABILISING_MS) {
if (this->update_interval_ <= STABILISING_MS) {
ESP_LOGCONFIG(TAG, " Mode: active continuous (sensor default)");
} else {
ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles");
@@ -55,44 +54,44 @@ void PMSX003Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
// Initialize sensor mode on first loop
if (this->initialised_ == 0) {
if (this->update_interval_ > PMS_STABILISING_MS) {
if (!this->initialised_) {
if (this->update_interval_ > STABILISING_MS) {
// Long update interval: use passive mode with sleep/wake cycles
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_PASSIVE);
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP);
this->send_command_(Command::MEASUREMENT_MODE, CMD_MEASUREMENT_MODE_PASSIVE);
this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_WAKEUP);
} else {
// Short/zero update interval: use active continuous mode
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_ACTIVE);
this->send_command_(Command::MEASUREMENT_MODE, CMD_MEASUREMENT_MODE_ACTIVE);
}
this->initialised_ = 1;
this->initialised_ = true;
}
// If we update less often than it takes the device to stabilise, spin the fan down
// rather than running it constantly. It does take some time to stabilise, so we
// need to keep track of what state we're in.
if (this->update_interval_ > PMS_STABILISING_MS) {
if (this->update_interval_ > STABILISING_MS) {
switch (this->state_) {
case PMSX003_STATE_IDLE:
case State::IDLE:
// Power on the sensor now so it'll be ready when we hit the update time
if (now - this->last_update_ < (this->update_interval_ - PMS_STABILISING_MS))
if (now - this->last_update_ < (this->update_interval_ - STABILISING_MS))
return;
this->state_ = PMSX003_STATE_STABILISING;
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP);
this->state_ = State::STABILISING;
this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_WAKEUP);
this->fan_on_time_ = now;
return;
case PMSX003_STATE_STABILISING:
case State::STABILISING:
// wait for the sensor to be stable
if (now - this->fan_on_time_ < PMS_STABILISING_MS)
if (now - this->fan_on_time_ < STABILISING_MS)
return;
// consume any command responses that are in the serial buffer
while (this->available())
this->read_byte(&this->data_[0]);
// Trigger a new read
this->send_command_(PMS_CMD_MANUAL_MEASUREMENT, 0);
this->state_ = PMSX003_STATE_WAITING;
this->send_command_(Command::MANUAL_MEASUREMENT, 0);
this->state_ = State::WAITING;
break;
case PMSX003_STATE_WAITING:
case State::WAITING:
// Just go ahead and read stuff
break;
}
@@ -180,27 +179,31 @@ 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 PMSX003_TYPE_X003:
// 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 PMSX003_TYPE_5003T:
case PMSX003_TYPE_5003S:
return payload_length == 28; // 2*13+2 (Data 13 not set/reserved)
case PMSX003_TYPE_5003ST:
return payload_length == 36; // 2*17+2 (Data 16 not set/reserved)
case Type::PMS5003S:
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;
}
void PMSX003Component::send_command_(PMSX0003Command cmd, uint16_t data) {
void PMSX003Component::send_command_(Command cmd, uint16_t data) {
uint8_t send_data[7] = {
START_CHARACTER_1, // Start Byte 1
START_CHARACTER_2, // Start Byte 2
cmd, // Command
static_cast<uint8_t>(cmd), // Command
uint8_t((data >> 8) & 0xFF), // Data 1
uint8_t((data >> 0) & 0xFF), // Data 2
0, // Verify Byte 1
@@ -265,7 +268,7 @@ void PMSX003Component::parse_data_() {
if (this->pm_particles_25um_sensor_ != nullptr)
this->pm_particles_25um_sensor_->publish_state(pm_particles_25um);
if (this->type_ == PMSX003_TYPE_5003T) {
if (this->type_ == Type::PMS5003T) {
ESP_LOGD(TAG,
"Got PM0.3 Particles: %u Count/0.1L, PM0.5 Particles: %u Count/0.1L, PM1.0 Particles: %u Count/0.1L, "
"PM2.5 Particles %u Count/0.1L",
@@ -289,7 +292,7 @@ void PMSX003Component::parse_data_() {
}
// Formaldehyde
if (this->type_ == PMSX003_TYPE_5003ST || this->type_ == PMSX003_TYPE_5003S) {
if (this->type_ == Type::PMS5003S || this->type_ == Type::PMS5003ST) {
const uint16_t formaldehyde = this->get_16_bit_uint_(28);
ESP_LOGD(TAG, "Got Formaldehyde: %u µg/m^3", formaldehyde);
@@ -299,8 +302,8 @@ void PMSX003Component::parse_data_() {
}
// Temperature and Humidity
if (this->type_ == PMSX003_TYPE_5003ST || this->type_ == PMSX003_TYPE_5003T) {
const uint8_t temperature_offset = (this->type_ == PMSX003_TYPE_5003T) ? 24 : 30;
if (this->type_ == Type::PMS5003T || this->type_ == Type::PMS5003ST) {
const uint8_t temperature_offset = (this->type_ == Type::PMS5003T) ? 24 : 30;
const float temperature = static_cast<int16_t>(this->get_16_bit_uint_(temperature_offset)) / 10.0f;
const float humidity = this->get_16_bit_uint_(temperature_offset + 2) / 10.0f;
@@ -314,22 +317,22 @@ void PMSX003Component::parse_data_() {
}
// Firmware Version and Error Code
if (this->type_ == PMSX003_TYPE_5003ST) {
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);
}
// Spin down the sensor again if we aren't going to need it until more time has
// passed than it takes to stabilise
if (this->update_interval_ > PMS_STABILISING_MS) {
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_SLEEP);
this->state_ = PMSX003_STATE_IDLE;
if (this->update_interval_ > STABILISING_MS) {
this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_SLEEP);
this->state_ = State::IDLE;
}
this->status_clear_warning();
}
} // namespace pmsx003
} // namespace esphome
} // namespace esphome::pmsx003

View File

@@ -5,27 +5,28 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace pmsx003 {
namespace esphome::pmsx003 {
enum PMSX0003Command : uint8_t {
PMS_CMD_MEASUREMENT_MODE =
0xE1, // Data Options: `PMS_CMD_MEASUREMENT_MODE_PASSIVE`, `PMS_CMD_MEASUREMENT_MODE_ACTIVE`
PMS_CMD_MANUAL_MEASUREMENT = 0xE2,
PMS_CMD_SLEEP_MODE = 0xE4, // Data Options: `PMS_CMD_SLEEP_MODE_SLEEP`, `PMS_CMD_SLEEP_MODE_WAKEUP`
enum class Type : uint8_t {
PMS1003 = 0,
PMS3003,
PMSX003, // PMS5003, PMS6003, PMS7003, PMSA003 (NOT PMSA003I - see `pmsa003i` component)
PMS5003S,
PMS5003T,
PMS5003ST,
PMS9003M,
};
enum PMSX003Type {
PMSX003_TYPE_X003 = 0,
PMSX003_TYPE_5003T,
PMSX003_TYPE_5003ST,
PMSX003_TYPE_5003S,
enum class Command : uint8_t {
MEASUREMENT_MODE = 0xE1, // Data Options: `CMD_MEASUREMENT_MODE_PASSIVE`, `CMD_MEASUREMENT_MODE_ACTIVE`
MANUAL_MEASUREMENT = 0xE2,
SLEEP_MODE = 0xE4, // Data Options: `CMD_SLEEP_MODE_SLEEP`, `CMD_SLEEP_MODE_WAKEUP`
};
enum PMSX003State {
PMSX003_STATE_IDLE = 0,
PMSX003_STATE_STABILISING,
PMSX003_STATE_WAITING,
enum class State : uint8_t {
IDLE = 0,
STABILISING,
WAITING,
};
class PMSX003Component : public uart::UARTDevice, public Component {
@@ -37,7 +38,7 @@ class PMSX003Component : public uart::UARTDevice, public Component {
void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
void set_type(PMSX003Type type) { this->type_ = type; }
void set_type(Type type) { this->type_ = type; }
void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor) { this->pm_1_0_std_sensor_ = pm_1_0_std_sensor; }
void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor) { this->pm_2_5_std_sensor_ = pm_2_5_std_sensor; }
@@ -77,20 +78,20 @@ class PMSX003Component : public uart::UARTDevice, public Component {
optional<bool> check_byte_();
void parse_data_();
bool check_payload_length_(uint16_t payload_length);
void send_command_(PMSX0003Command cmd, uint16_t data);
void send_command_(Command cmd, uint16_t data);
uint16_t get_16_bit_uint_(uint8_t start_index) const {
return encode_uint16(this->data_[start_index], this->data_[start_index + 1]);
}
Type type_;
State state_{State::IDLE};
bool initialised_{false};
uint8_t data_[64];
uint8_t data_index_{0};
uint8_t initialised_{0};
uint32_t fan_on_time_{0};
uint32_t last_update_{0};
uint32_t last_transmission_{0};
uint32_t update_interval_{0};
PMSX003State state_{PMSX003_STATE_IDLE};
PMSX003Type type_;
// "Standard Particle"
sensor::Sensor *pm_1_0_std_sensor_{nullptr};
@@ -118,5 +119,4 @@ class PMSX003Component : public uart::UARTDevice, public Component {
sensor::Sensor *humidity_sensor_{nullptr};
};
} // namespace pmsx003
} // namespace esphome
} // namespace esphome::pmsx003

View File

@@ -40,34 +40,128 @@ 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_PMS5003S = "PMS5003S"
TYPE_PMS9003M = "PMS9003M"
PMSX003Type = pmsx003_ns.enum("PMSX003Type")
Type = pmsx003_ns.enum("Type", is_class=True)
PMSX003_TYPES = {
TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003,
TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T,
TYPE_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST,
TYPE_PMS5003S: PMSX003Type.PMSX003_TYPE_5003S,
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: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
CONF_PM_1_0_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
CONF_PM_2_5_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
CONF_PM_10_0_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
CONF_PM_0_3UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
CONF_PM_0_5UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
CONF_PM_1_0UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
CONF_PM_2_5UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
CONF_PM_5_0UM: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S],
CONF_PM_10_0UM: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S],
CONF_FORMALDEHYDE: [TYPE_PMS5003ST, TYPE_PMS5003S],
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],
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

@@ -8,11 +8,11 @@ sensor:
pm_10_0:
name: PM 10.0 Concentration
pm_1_0_std:
name: PM 1.0 Standard Atmospher Concentration
name: PM 1.0 Standard Atmospheric Concentration
pm_2_5_std:
name: PM 2.5 Standard Atmospher Concentration
name: PM 2.5 Standard Atmospheric Concentration
pm_10_0_std:
name: PM 10.0 Standard Atmospher Concentration
name: PM 10.0 Standard Atmospheric Concentration
pm_0_3um:
name: Particulate Count >0.3um
pm_0_5um:

View File

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