Compare commits

...

13 Commits

Author SHA1 Message Date
J. Nick Koston
c08726036e Merge branch 'dev' into peername_no_double_ram 2026-01-30 20:13:13 -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
d602a2e5e4 compile tmie safety at higheer level 2026-01-26 08:44:06 -10:00
J. Nick Koston
dcab12adae isra 2026-01-25 20:03:44 -10:00
J. Nick Koston
fb714636e3 missed 2026-01-25 20:02:46 -10:00
J. Nick Koston
05a431ea54 fixup 2026-01-25 20:02:46 -10:00
J. Nick Koston
1a34b4e7d7 [api] Remove duplicate peername storage to save RAM 2026-01-25 18:17:47 -10:00
24 changed files with 655 additions and 173 deletions

View File

@@ -1 +1 @@
cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3

View File

@@ -133,8 +133,8 @@ void APIConnection::start() {
return;
}
// Initialize client name with peername (IP address) until Hello message provides actual name
const char *peername = this->helper_->get_client_peername();
this->helper_->set_client_name(peername, strlen(peername));
char peername[socket::SOCKADDR_STR_LEN];
this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
}
APIConnection::~APIConnection() {
@@ -179,8 +179,8 @@ void APIConnection::begin_iterator_(ActiveIterator type) {
void APIConnection::loop() {
if (this->flags_.next_close) {
// requested a disconnect
this->helper_->close();
// requested a disconnect - don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
this->flags_.remove = true;
return;
}
@@ -293,7 +293,8 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
}
void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
this->helper_->close();
// Don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
this->flags_.remove = true;
}
@@ -1524,8 +1525,11 @@ void APIConnection::complete_authentication_() {
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_client_peername()));
{
char peername[socket::SOCKADDR_STR_LEN];
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_peername_to(peername)));
}
#endif
#ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1544,8 +1548,9 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor;
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp;
resp.api_version_major = 1;
@@ -1862,7 +1867,8 @@ void APIConnection::on_no_setup_connection() {
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
}
void APIConnection::on_fatal_error() {
this->helper_->close();
// Don't close socket here - keep it open so getpeername() works for logging
// Socket will be closed when client is removed from the list in APIServer::loop()
this->flags_.remove = true;
}
@@ -2218,12 +2224,14 @@ void APIConnection::process_state_subscriptions_() {
#endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_client_(int level, const LogString *message) {
char peername[socket::SOCKADDR_STR_LEN];
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
this->helper_->get_client_peername(), LOG_STR_ARG(message));
this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
}
void APIConnection::log_warning_(const LogString *message, APIError err) {
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(),
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_peername_to(peername),
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
}

View File

@@ -281,8 +281,10 @@ class APIConnection final : public APIServerConnection {
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const char *get_name() const { return this->helper_->get_client_name(); }
/// Get peer name (IP address) - cached at connection init time
const char *get_peername() const { return this->helper_->get_client_peername(); }
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
return this->helper_->get_peername_to(buf);
}
protected:
// Helper function to handle authentication completion

View File

@@ -16,7 +16,12 @@ static const char *const TAG = "api.frame_helper";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
@@ -240,13 +245,20 @@ APIError APIFrameHelper::try_send_tx_buf_() {
return APIError::OK; // All buffers sent successfully
}
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
if (this->socket_) {
this->socket_->getpeername_to(buf);
} else {
buf[0] = '\0';
}
return buf.data();
}
APIError APIFrameHelper::init_common_() {
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
// Cache peername now while socket is valid - needed for error logging after socket failure
this->socket_->getpeername_to(this->client_peername_);
int err = this->socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;

View File

@@ -90,8 +90,9 @@ class APIFrameHelper {
// Get client name (null-terminated)
const char *get_client_name() const { return this->client_name_; }
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
const char *get_client_peername() const { return this->client_peername_; }
// Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
// Returns pointer to buf for convenience in printf-style calls
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const;
// Set client name from buffer with length (truncates if needed)
void set_client_name(const char *name, size_t len) {
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
@@ -105,6 +106,8 @@ class APIFrameHelper {
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
if (state_ == State::CLOSED)
return APIError::OK; // Already closed
state_ = State::CLOSED;
int err = this->socket_->close();
if (err == -1)
@@ -231,8 +234,6 @@ class APIFrameHelper {
// Client name buffer - stores name from Hello message or initial peername
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
// Cached peername/IP address - captured at init time for availability after socket failure
char client_peername_[socket::SOCKADDR_STR_LEN]{};
// Group smaller types together
uint16_t rx_buf_len_ = 0;

View File

@@ -29,7 +29,12 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -21,7 +21,12 @@ static const char *const TAG = "api.plaintext";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -192,11 +192,15 @@ void APIServer::loop() {
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before removal for the trigger
// Save client info before closing socket and removal for the trigger
char peername_buf[socket::SOCKADDR_STR_LEN];
std::string client_name(client->get_name());
std::string client_peername(client->get_peername());
std::string client_peername(client->get_peername_to(peername_buf));
#endif
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());

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

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

@@ -430,12 +430,14 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr
}
if (this->api_client_ != nullptr) {
char current_peername[socket::SOCKADDR_STR_LEN];
char new_peername[socket::SOCKADDR_STR_LEN];
ESP_LOGE(TAG,
"Multiple API Clients attempting to connect to Voice Assistant\n"
"Current client: %s (%s)\n"
"New client: %s (%s)",
this->api_client_->get_name(), this->api_client_->get_peername(), client->get_name(),
client->get_peername());
this->api_client_->get_name(), this->api_client_->get_peername_to(current_peername), client->get_name(),
client->get_peername_to(new_peername));
return;
}

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