1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-12 02:32:15 +00:00

Compare commits

..

17 Commits

Author SHA1 Message Date
J. Nick Koston
aa870483f1 Reduce SerializationBuffer stack size from 768 to 512 bytes
768 bytes on the httpd task stack contributes to stack overflow when
combined with other stack-allocated buffers (query string parsing, etc.).
512 bytes still covers all typical JSON payloads (sensors ~200B, lights
~170B, climate ~500-700B). Only extreme edge cases with 40+ options
trigger heap fallback.
2026-02-11 18:58:28 -06:00
J. Nick Koston
840ad30880 Merge branch 'dev' into json_web_server_stack 2026-01-29 14:48:48 -10:00
J. Nick Koston
cfe121b38b private implementation details 2026-01-29 18:45:18 -06:00
J. Nick Koston
5fbd9d5b14 avoid misuse 2026-01-29 18:23:25 -06:00
J. Nick Koston
2b1783ce61 tweak 2026-01-29 18:19:29 -06:00
J. Nick Koston
904072ce79 make sure NRVO works 2026-01-29 18:17:34 -06:00
J. Nick Koston
0a4b98d74a fix double templates 2026-01-29 17:43:26 -06:00
J. Nick Koston
b8017de724 avoid regressing performance of mqtt 2026-01-29 16:55:20 -06:00
J. Nick Koston
ca96604582 change all the cases 2026-01-29 16:49:28 -06:00
J. Nick Koston
d18d378f06 missed a few 2026-01-29 16:27:28 -06:00
J. Nick Koston
83e3752544 missed a few 2026-01-29 16:26:50 -06:00
J. Nick Koston
0490b2d450 no dummy 2026-01-29 16:24:30 -06:00
J. Nick Koston
55ff740e4e no dummy 2026-01-29 16:23:41 -06:00
J. Nick Koston
aba8a83cba ard as well 2026-01-29 16:02:32 -06:00
J. Nick Koston
a23809d5db fixes 2026-01-29 15:41:29 -06:00
J. Nick Koston
32fc3ea6f5 config stack 2026-01-29 15:33:24 -06:00
J. Nick Koston
deb8ffd348 json webserver stack 2026-01-29 15:26:37 -06:00
66 changed files with 568 additions and 2086 deletions

View File

@@ -1 +1 @@
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3 cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab

View File

@@ -134,7 +134,6 @@ esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber esphome/components/dfrobot_sen0395/* @niklasweber
esphome/components/dht/* @OttoWinter esphome/components/dht/* @OttoWinter
esphome/components/display_menu_base/* @numo68 esphome/components/display_menu_base/* @numo68
esphome/components/dlms_meter/* @SimonFischer04
esphome/components/dps310/* @kbx81 esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds2484/* @mrk-its esphome/components/ds2484/* @mrk-its

View File

@@ -264,9 +264,9 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
// Build and send JSON response // Build and send JSON response
json::JsonBuilder builder; json::JsonBuilder builder;
this->json_builder_(x..., builder.root()); this->json_builder_(x..., builder.root());
std::string json_str = builder.serialize(); auto json_buf = builder.serialize();
this->parent_->send_action_response(call_id, success, StringRef(error_message), this->parent_->send_action_response(call_id, success, StringRef(error_message),
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size()); reinterpret_cast<const uint8_t *>(json_buf.data()), json_buf.size());
return; return;
} }
#endif #endif

View File

@@ -1,57 +0,0 @@
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
CODEOWNERS = ["@SimonFischer04"]
DEPENDENCIES = ["uart"]
CONF_DLMS_METER_ID = "dlms_meter_id"
CONF_DECRYPTION_KEY = "decryption_key"
CONF_PROVIDER = "provider"
PROVIDERS = {"generic": 0, "netznoe": 1}
dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter")
DlmsMeterComponent = dlms_meter_component_ns.class_(
"DlmsMeterComponent", cg.Component, uart.UARTDevice
)
def validate_key(value):
value = cv.string_strict(value)
if len(value) != 32:
raise cv.Invalid("Decryption key must be 32 hex characters (16 bytes)")
try:
return [int(value[i : i + 2], 16) for i in range(0, 32, 2)]
except ValueError as exc:
raise cv.Invalid("Decryption key must be hex values from 00 to FF") from exc
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DlmsMeterComponent),
cv.Required(CONF_DECRYPTION_KEY): validate_key,
cv.Optional(CONF_PROVIDER, default="generic"): cv.enum(
PROVIDERS, lower=True
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP8266, PLATFORM_ESP32]),
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"dlms_meter", baud_rate=2400, require_rx=True
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
key = ", ".join(str(b) for b in config[CONF_DECRYPTION_KEY])
cg.add(var.set_decryption_key(cg.RawExpression(f"{{{key}}}")))
cg.add(var.set_provider(PROVIDERS[config[CONF_PROVIDER]]))

View File

@@ -1,71 +0,0 @@
#pragma once
#include <cstdint>
namespace esphome::dlms_meter {
/*
+-------------------------------+
| Ciphering Service |
+-------------------------------+
| System Title Length |
+-------------------------------+
| |
| |
| |
| System |
| Title |
| |
| |
| |
+-------------------------------+
| Length | (1 or 3 Bytes)
+-------------------------------+
| Security Control Byte |
+-------------------------------+
| |
| Frame |
| Counter |
| |
+-------------------------------+
| |
~ ~
Encrypted Payload
~ ~
| |
+-------------------------------+
Ciphering Service: 0xDB (General-Glo-Ciphering)
System Title Length: 0x08
System Title: Unique ID of meter
Length: 1 Byte=Length <= 127, 3 Bytes=Length > 127 (0x82 & 2 Bytes length)
Security Control Byte:
- Bit 3…0: Security_Suite_Id
- Bit 4: "A" subfield: indicates that authentication is applied
- Bit 5: "E" subfield: indicates that encryption is applied
- Bit 6: Key_Set subfield: 0 = Unicast, 1 = Broadcast
- Bit 7: Indicates the use of compression.
*/
static constexpr uint8_t DLMS_HEADER_LENGTH = 16;
static constexpr uint8_t DLMS_HEADER_EXT_OFFSET = 2; // Extra offset for extended length header
static constexpr uint8_t DLMS_CIPHER_OFFSET = 0;
static constexpr uint8_t DLMS_SYST_OFFSET = 1;
static constexpr uint8_t DLMS_LENGTH_OFFSET = 10;
static constexpr uint8_t TWO_BYTE_LENGTH = 0x82;
static constexpr uint8_t DLMS_LENGTH_CORRECTION = 5; // Header bytes included in length field
static constexpr uint8_t DLMS_SECBYTE_OFFSET = 11;
static constexpr uint8_t DLMS_FRAMECOUNTER_OFFSET = 12;
static constexpr uint8_t DLMS_FRAMECOUNTER_LENGTH = 4;
static constexpr uint8_t DLMS_PAYLOAD_OFFSET = 16;
static constexpr uint8_t GLO_CIPHERING = 0xDB;
static constexpr uint8_t DATA_NOTIFICATION = 0x0F;
static constexpr uint8_t TIMESTAMP_DATETIME = 0x0C;
static constexpr uint16_t MAX_MESSAGE_LENGTH = 512; // Maximum size of message (when having 2 bytes length in header).
// Provider specific quirks
static constexpr uint8_t NETZ_NOE_MAGIC_BYTE = 0x81; // Magic length byte used by Netz NOE
static constexpr uint8_t NETZ_NOE_EXPECTED_MESSAGE_LENGTH = 0xF8;
static constexpr uint8_t NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE = 0x20;
} // namespace esphome::dlms_meter

View File

@@ -1,468 +0,0 @@
#include "dlms_meter.h"
#include <cmath>
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
#include <bearssl/bearssl.h>
#elif defined(USE_ESP32)
#include "mbedtls/esp_config.h"
#include "mbedtls/gcm.h"
#endif
namespace esphome::dlms_meter {
static constexpr const char *TAG = "dlms_meter";
void DlmsMeterComponent::dump_config() {
const char *provider_name = this->provider_ == PROVIDER_NETZNOE ? "Netz NOE" : "Generic";
ESP_LOGCONFIG(TAG,
"DLMS Meter:\n"
" Provider: %s\n"
" Read Timeout: %u ms",
provider_name, this->read_timeout_);
#define DLMS_METER_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s##_sensor_);
DLMS_METER_SENSOR_LIST(DLMS_METER_LOG_SENSOR, )
#define DLMS_METER_LOG_TEXT_SENSOR(s) LOG_TEXT_SENSOR(" ", #s, this->s##_text_sensor_);
DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_LOG_TEXT_SENSOR, )
}
void DlmsMeterComponent::loop() {
// Read while data is available, netznoe uses two frames so allow 2x max frame length
while (this->available()) {
if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) {
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
break;
}
uint8_t c;
this->read_byte(&c);
this->receive_buffer_.push_back(c);
this->last_read_ = millis();
}
if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) {
this->mbus_payload_.clear();
if (!this->parse_mbus_(this->mbus_payload_))
return;
uint16_t message_length;
uint8_t systitle_length;
uint16_t header_offset;
if (!this->parse_dlms_(this->mbus_payload_, message_length, systitle_length, header_offset))
return;
if (message_length < DECODER_START_OFFSET || message_length > MAX_MESSAGE_LENGTH) {
ESP_LOGE(TAG, "DLMS: Message length invalid: %u", message_length);
this->receive_buffer_.clear();
return;
}
// Decrypt in place and then decode the OBIS codes
if (!this->decrypt_(this->mbus_payload_, message_length, systitle_length, header_offset))
return;
this->decode_obis_(&this->mbus_payload_[header_offset + DLMS_PAYLOAD_OFFSET], message_length);
}
}
bool DlmsMeterComponent::parse_mbus_(std::vector<uint8_t> &mbus_payload) {
ESP_LOGV(TAG, "Parsing M-Bus frames");
uint16_t frame_offset = 0; // Offset is used if the M-Bus message is split into multiple frames
while (frame_offset < this->receive_buffer_.size()) {
// Ensure enough bytes remain for the minimal intro header before accessing indices
if (this->receive_buffer_.size() - frame_offset < MBUS_HEADER_INTRO_LENGTH) {
ESP_LOGE(TAG, "MBUS: Not enough data for frame header (need %d, have %d)", MBUS_HEADER_INTRO_LENGTH,
(this->receive_buffer_.size() - frame_offset));
this->receive_buffer_.clear();
return false;
}
// Check start bytes
if (this->receive_buffer_[frame_offset + MBUS_START1_OFFSET] != START_BYTE_LONG_FRAME ||
this->receive_buffer_[frame_offset + MBUS_START2_OFFSET] != START_BYTE_LONG_FRAME) {
ESP_LOGE(TAG, "MBUS: Start bytes do not match");
this->receive_buffer_.clear();
return false;
}
// Both length bytes must be identical
if (this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET] !=
this->receive_buffer_[frame_offset + MBUS_LENGTH2_OFFSET]) {
ESP_LOGE(TAG, "MBUS: Length bytes do not match");
this->receive_buffer_.clear();
return false;
}
uint8_t frame_length = this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET]; // Get length of this frame
// Check if received data is enough for the given frame length
if (this->receive_buffer_.size() - frame_offset <
frame_length + 3) { // length field inside packet does not account for second start- + checksum- + stop- byte
ESP_LOGE(TAG, "MBUS: Frame too big for received data");
this->receive_buffer_.clear();
return false;
}
// Ensure we have full frame (header + payload + checksum + stop byte) before accessing stop byte
size_t required_total =
frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH; // payload + header + 2 footer bytes
if (this->receive_buffer_.size() - frame_offset < required_total) {
ESP_LOGE(TAG, "MBUS: Incomplete frame (need %d, have %d)", (unsigned int) required_total,
this->receive_buffer_.size() - frame_offset);
this->receive_buffer_.clear();
return false;
}
if (this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH - 1] !=
STOP_BYTE) {
ESP_LOGE(TAG, "MBUS: Invalid stop byte");
this->receive_buffer_.clear();
return false;
}
// Verify checksum: sum of all bytes starting at MBUS_HEADER_INTRO_LENGTH, take last byte
uint8_t checksum = 0; // use uint8_t so only the 8 least significant bits are stored
for (uint16_t i = 0; i < frame_length; i++) {
checksum += this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + i];
}
if (checksum != this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]) {
ESP_LOGE(TAG, "MBUS: Invalid checksum: %x != %x", checksum,
this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]);
this->receive_buffer_.clear();
return false;
}
mbus_payload.insert(mbus_payload.end(), &this->receive_buffer_[frame_offset + MBUS_FULL_HEADER_LENGTH],
&this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + frame_length]);
frame_offset += MBUS_HEADER_INTRO_LENGTH + frame_length + MBUS_FOOTER_LENGTH;
}
return true;
}
bool DlmsMeterComponent::parse_dlms_(const std::vector<uint8_t> &mbus_payload, uint16_t &message_length,
uint8_t &systitle_length, uint16_t &header_offset) {
ESP_LOGV(TAG, "Parsing DLMS header");
if (mbus_payload.size() < DLMS_HEADER_LENGTH + DLMS_HEADER_EXT_OFFSET) {
ESP_LOGE(TAG, "DLMS: Payload too short");
this->receive_buffer_.clear();
return false;
}
if (mbus_payload[DLMS_CIPHER_OFFSET] != GLO_CIPHERING) { // Only general-glo-ciphering is supported (0xDB)
ESP_LOGE(TAG, "DLMS: Unsupported cipher");
this->receive_buffer_.clear();
return false;
}
systitle_length = mbus_payload[DLMS_SYST_OFFSET];
if (systitle_length != 0x08) { // Only system titles with length of 8 are supported
ESP_LOGE(TAG, "DLMS: Unsupported system title length");
this->receive_buffer_.clear();
return false;
}
message_length = mbus_payload[DLMS_LENGTH_OFFSET];
header_offset = 0;
if (this->provider_ == PROVIDER_NETZNOE) {
// for some reason EVN seems to set the standard "length" field to 0x81 and then the actual length is in the next
// byte. Check some bytes to see if received data still matches expectation
if (message_length == NETZ_NOE_MAGIC_BYTE &&
mbus_payload[DLMS_LENGTH_OFFSET + 1] == NETZ_NOE_EXPECTED_MESSAGE_LENGTH &&
mbus_payload[DLMS_LENGTH_OFFSET + 2] == NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE) {
message_length = mbus_payload[DLMS_LENGTH_OFFSET + 1];
header_offset = 1;
} else {
ESP_LOGE(TAG, "Wrong Length - Security Control Byte sequence detected for provider EVN");
}
} else {
if (message_length == TWO_BYTE_LENGTH) {
message_length = encode_uint16(mbus_payload[DLMS_LENGTH_OFFSET + 1], mbus_payload[DLMS_LENGTH_OFFSET + 2]);
header_offset = DLMS_HEADER_EXT_OFFSET;
}
}
if (message_length < DLMS_LENGTH_CORRECTION) {
ESP_LOGE(TAG, "DLMS: Message length too short: %u", message_length);
this->receive_buffer_.clear();
return false;
}
message_length -= DLMS_LENGTH_CORRECTION; // Correct message length due to part of header being included in length
if (mbus_payload.size() - DLMS_HEADER_LENGTH - header_offset != message_length) {
ESP_LOGV(TAG, "DLMS: Length mismatch - payload=%d, header=%d, offset=%d, message=%d", mbus_payload.size(),
DLMS_HEADER_LENGTH, header_offset, message_length);
ESP_LOGE(TAG, "DLMS: Message has invalid length");
this->receive_buffer_.clear();
return false;
}
if (mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] != 0x21 &&
mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] !=
0x20) { // Only certain security suite is supported (0x21 || 0x20)
ESP_LOGE(TAG, "DLMS: Unsupported security control byte");
this->receive_buffer_.clear();
return false;
}
return true;
}
bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t message_length, uint8_t systitle_length,
uint16_t header_offset) {
ESP_LOGV(TAG, "Decrypting payload");
uint8_t iv[12]; // Reserve space for the IV, always 12 bytes
// Copy system title to IV (System title is before length; no header offset needed!)
// Add 1 to the offset in order to skip the system title length byte
memcpy(&iv[0], &mbus_payload[DLMS_SYST_OFFSET + 1], systitle_length);
memcpy(&iv[8], &mbus_payload[header_offset + DLMS_FRAMECOUNTER_OFFSET],
DLMS_FRAMECOUNTER_LENGTH); // Copy frame counter to IV
uint8_t *payload_ptr = &mbus_payload[header_offset + DLMS_PAYLOAD_OFFSET];
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
br_gcm_context gcm_ctx;
br_aes_ct_ctr_keys bc;
br_aes_ct_ctr_init(&bc, this->decryption_key_.data(), this->decryption_key_.size());
br_gcm_init(&gcm_ctx, &bc.vtable, br_ghash_ctmul32);
br_gcm_reset(&gcm_ctx, iv, sizeof(iv));
br_gcm_flip(&gcm_ctx);
br_gcm_run(&gcm_ctx, 0, payload_ptr, message_length);
#elif defined(USE_ESP32)
size_t outlen = 0;
mbedtls_gcm_context gcm_ctx;
mbedtls_gcm_init(&gcm_ctx);
mbedtls_gcm_setkey(&gcm_ctx, MBEDTLS_CIPHER_ID_AES, this->decryption_key_.data(), this->decryption_key_.size() * 8);
mbedtls_gcm_starts(&gcm_ctx, MBEDTLS_GCM_DECRYPT, iv, sizeof(iv));
auto ret = mbedtls_gcm_update(&gcm_ctx, payload_ptr, message_length, payload_ptr, message_length, &outlen);
mbedtls_gcm_free(&gcm_ctx);
if (ret != 0) {
ESP_LOGE(TAG, "Decryption failed with error: %d", ret);
this->receive_buffer_.clear();
return false;
}
#else
#error "Invalid Platform"
#endif
if (payload_ptr[0] != DATA_NOTIFICATION || payload_ptr[5] != TIMESTAMP_DATETIME) {
ESP_LOGE(TAG, "OBIS: Packet was decrypted but data is invalid");
this->receive_buffer_.clear();
return false;
}
ESP_LOGV(TAG, "Decrypted payload: %d bytes", message_length);
return true;
}
void DlmsMeterComponent::decode_obis_(uint8_t *plaintext, uint16_t message_length) {
ESP_LOGV(TAG, "Decoding payload");
MeterData data{};
uint16_t current_position = DECODER_START_OFFSET;
bool power_factor_found = false;
while (current_position + OBIS_CODE_OFFSET <= message_length) {
if (plaintext[current_position + OBIS_TYPE_OFFSET] != DataType::OCTET_STRING) {
ESP_LOGE(TAG, "OBIS: Unsupported OBIS header type: %x", plaintext[current_position + OBIS_TYPE_OFFSET]);
this->receive_buffer_.clear();
return;
}
uint8_t obis_code_length = plaintext[current_position + OBIS_LENGTH_OFFSET];
if (obis_code_length != OBIS_CODE_LENGTH_STANDARD && obis_code_length != OBIS_CODE_LENGTH_EXTENDED) {
ESP_LOGE(TAG, "OBIS: Unsupported OBIS header length: %x", obis_code_length);
this->receive_buffer_.clear();
return;
}
if (current_position + OBIS_CODE_OFFSET + obis_code_length > message_length) {
ESP_LOGE(TAG, "OBIS: Buffer too short for OBIS code");
this->receive_buffer_.clear();
return;
}
uint8_t *obis_code = &plaintext[current_position + OBIS_CODE_OFFSET];
uint8_t obis_medium = obis_code[OBIS_A];
uint16_t obis_cd = encode_uint16(obis_code[OBIS_C], obis_code[OBIS_D]);
bool timestamp_found = false;
bool meter_number_found = false;
if (this->provider_ == PROVIDER_NETZNOE) {
// Do not advance Position when reading the Timestamp at DECODER_START_OFFSET
if ((obis_code_length == OBIS_CODE_LENGTH_EXTENDED) && (current_position == DECODER_START_OFFSET)) {
timestamp_found = true;
} else if (power_factor_found) {
meter_number_found = true;
power_factor_found = false;
} else {
current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code and position
}
} else {
current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code, position and type
}
if (!timestamp_found && !meter_number_found && obis_medium != Medium::ELECTRICITY &&
obis_medium != Medium::ABSTRACT) {
ESP_LOGE(TAG, "OBIS: Unsupported OBIS medium: %x", obis_medium);
this->receive_buffer_.clear();
return;
}
if (current_position >= message_length) {
ESP_LOGE(TAG, "OBIS: Buffer too short for data type");
this->receive_buffer_.clear();
return;
}
float value = 0.0f;
uint8_t value_size = 0;
uint8_t data_type = plaintext[current_position];
current_position++;
switch (data_type) {
case DataType::DOUBLE_LONG_UNSIGNED: {
value_size = 4;
if (current_position + value_size > message_length) {
ESP_LOGE(TAG, "OBIS: Buffer too short for DOUBLE_LONG_UNSIGNED");
this->receive_buffer_.clear();
return;
}
value = encode_uint32(plaintext[current_position + 0], plaintext[current_position + 1],
plaintext[current_position + 2], plaintext[current_position + 3]);
current_position += value_size;
break;
}
case DataType::LONG_UNSIGNED: {
value_size = 2;
if (current_position + value_size > message_length) {
ESP_LOGE(TAG, "OBIS: Buffer too short for LONG_UNSIGNED");
this->receive_buffer_.clear();
return;
}
value = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]);
current_position += value_size;
break;
}
case DataType::OCTET_STRING: {
uint8_t data_length = plaintext[current_position];
current_position++; // Advance past string length
if (current_position + data_length > message_length) {
ESP_LOGE(TAG, "OBIS: Buffer too short for OCTET_STRING");
this->receive_buffer_.clear();
return;
}
// Handle timestamp (normal OBIS code or NETZNOE special case)
if (obis_cd == OBIS_TIMESTAMP || timestamp_found) {
if (data_length < 8) {
ESP_LOGE(TAG, "OBIS: Timestamp data too short: %u", data_length);
this->receive_buffer_.clear();
return;
}
uint16_t year = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]);
uint8_t month = plaintext[current_position + 2];
uint8_t day = plaintext[current_position + 3];
uint8_t hour = plaintext[current_position + 5];
uint8_t minute = plaintext[current_position + 6];
uint8_t second = plaintext[current_position + 7];
if (year > 9999 || month > 12 || day > 31 || hour > 23 || minute > 59 || second > 59) {
ESP_LOGE(TAG, "Invalid timestamp values: %04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour, minute,
second);
this->receive_buffer_.clear();
return;
}
snprintf(data.timestamp, sizeof(data.timestamp), "%04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour,
minute, second);
} else if (meter_number_found) {
snprintf(data.meternumber, sizeof(data.meternumber), "%.*s", data_length, &plaintext[current_position]);
}
current_position += data_length;
break;
}
default:
ESP_LOGE(TAG, "OBIS: Unsupported OBIS data type: %x", data_type);
this->receive_buffer_.clear();
return;
}
// Skip break after data
if (this->provider_ == PROVIDER_NETZNOE) {
// Don't skip the break on the first timestamp, as there's none
if (!timestamp_found) {
current_position += 2;
}
} else {
current_position += 2;
}
// Check for additional data (scaler-unit structure)
if (current_position < message_length && plaintext[current_position] == DataType::INTEGER) {
// Apply scaler: real_value = raw_value × 10^scaler
if (current_position + 1 < message_length) {
int8_t scaler = static_cast<int8_t>(plaintext[current_position + 1]);
if (scaler != 0) {
value *= powf(10.0f, scaler);
}
}
// on EVN Meters there is no additional break
if (this->provider_ == PROVIDER_NETZNOE) {
current_position += 4;
} else {
current_position += 6;
}
}
// Handle numeric values (LONG_UNSIGNED and DOUBLE_LONG_UNSIGNED)
if (value_size > 0) {
switch (obis_cd) {
case OBIS_VOLTAGE_L1:
data.voltage_l1 = value;
break;
case OBIS_VOLTAGE_L2:
data.voltage_l2 = value;
break;
case OBIS_VOLTAGE_L3:
data.voltage_l3 = value;
break;
case OBIS_CURRENT_L1:
data.current_l1 = value;
break;
case OBIS_CURRENT_L2:
data.current_l2 = value;
break;
case OBIS_CURRENT_L3:
data.current_l3 = value;
break;
case OBIS_ACTIVE_POWER_PLUS:
data.active_power_plus = value;
break;
case OBIS_ACTIVE_POWER_MINUS:
data.active_power_minus = value;
break;
case OBIS_ACTIVE_ENERGY_PLUS:
data.active_energy_plus = value;
break;
case OBIS_ACTIVE_ENERGY_MINUS:
data.active_energy_minus = value;
break;
case OBIS_REACTIVE_ENERGY_PLUS:
data.reactive_energy_plus = value;
break;
case OBIS_REACTIVE_ENERGY_MINUS:
data.reactive_energy_minus = value;
break;
case OBIS_POWER_FACTOR:
data.power_factor = value;
power_factor_found = true;
break;
default:
ESP_LOGW(TAG, "Unsupported OBIS code 0x%04X", obis_cd);
}
}
}
this->receive_buffer_.clear();
ESP_LOGI(TAG, "Received valid data");
this->publish_sensors(data);
this->status_clear_warning();
}
} // namespace esphome::dlms_meter

View File

@@ -1,96 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#include "esphome/components/uart/uart.h"
#include "mbus.h"
#include "dlms.h"
#include "obis.h"
#include <array>
#include <vector>
namespace esphome::dlms_meter {
#ifndef DLMS_METER_SENSOR_LIST
#define DLMS_METER_SENSOR_LIST(F, SEP)
#endif
#ifndef DLMS_METER_TEXT_SENSOR_LIST
#define DLMS_METER_TEXT_SENSOR_LIST(F, SEP)
#endif
struct MeterData {
float voltage_l1 = 0.0f; // Voltage L1
float voltage_l2 = 0.0f; // Voltage L2
float voltage_l3 = 0.0f; // Voltage L3
float current_l1 = 0.0f; // Current L1
float current_l2 = 0.0f; // Current L2
float current_l3 = 0.0f; // Current L3
float active_power_plus = 0.0f; // Active power taken from grid
float active_power_minus = 0.0f; // Active power put into grid
float active_energy_plus = 0.0f; // Active energy taken from grid
float active_energy_minus = 0.0f; // Active energy put into grid
float reactive_energy_plus = 0.0f; // Reactive energy taken from grid
float reactive_energy_minus = 0.0f; // Reactive energy put into grid
char timestamp[27]{}; // Text sensor for the timestamp value
// Netz NOE
float power_factor = 0.0f; // Power Factor
char meternumber[13]{}; // Text sensor for the meterNumber value
};
// Provider constants
enum Providers : uint32_t { PROVIDER_GENERIC = 0x00, PROVIDER_NETZNOE = 0x01 };
class DlmsMeterComponent : public Component, public uart::UARTDevice {
public:
DlmsMeterComponent() = default;
void dump_config() override;
void loop() override;
void set_decryption_key(const std::array<uint8_t, 16> &key) { this->decryption_key_ = key; }
void set_provider(uint32_t provider) { this->provider_ = provider; }
void publish_sensors(MeterData &data) {
#define DLMS_METER_PUBLISH_SENSOR(s) \
if (this->s##_sensor_ != nullptr) \
s##_sensor_->publish_state(data.s);
DLMS_METER_SENSOR_LIST(DLMS_METER_PUBLISH_SENSOR, )
#define DLMS_METER_PUBLISH_TEXT_SENSOR(s) \
if (this->s##_text_sensor_ != nullptr) \
s##_text_sensor_->publish_state(data.s);
DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_PUBLISH_TEXT_SENSOR, )
}
DLMS_METER_SENSOR_LIST(SUB_SENSOR, )
DLMS_METER_TEXT_SENSOR_LIST(SUB_TEXT_SENSOR, )
protected:
bool parse_mbus_(std::vector<uint8_t> &mbus_payload);
bool parse_dlms_(const std::vector<uint8_t> &mbus_payload, uint16_t &message_length, uint8_t &systitle_length,
uint16_t &header_offset);
bool decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t message_length, uint8_t systitle_length,
uint16_t header_offset);
void decode_obis_(uint8_t *plaintext, uint16_t message_length);
std::vector<uint8_t> receive_buffer_; // Stores the packet currently being received
std::vector<uint8_t> mbus_payload_; // Parsed M-Bus payload, reused to avoid heap churn
uint32_t last_read_ = 0; // Timestamp when data was last read
uint32_t read_timeout_ = 1000; // Time to wait after last byte before considering data complete
uint32_t provider_ = PROVIDER_GENERIC; // Provider of the meter / your grid operator
std::array<uint8_t, 16> decryption_key_;
};
} // namespace esphome::dlms_meter

View File

@@ -1,69 +0,0 @@
#pragma once
#include <cstdint>
namespace esphome::dlms_meter {
/*
+----------------------------------------------------+ -
| Start Character [0x68] | \
+----------------------------------------------------+ |
| Data Length (L) | |
+----------------------------------------------------+ |
| Data Length Repeat (L) | |
+----------------------------------------------------+ > M-Bus Data link layer
| Start Character Repeat [0x68] | |
+----------------------------------------------------+ |
| Control/Function Field (C) | |
+----------------------------------------------------+ |
| Address Field (A) | /
+----------------------------------------------------+ -
| Control Information Field (CI) | \
+----------------------------------------------------+ |
| Source Transport Service Access Point (STSAP) | > DLMS/COSEM M-Bus transport layer
+----------------------------------------------------+ |
| Destination Transport Service Access Point (DTSAP) | /
+----------------------------------------------------+ -
| | \
~ ~ |
Data > DLMS/COSEM Application Layer
~ ~ |
| | /
+----------------------------------------------------+ -
| Checksum | \
+----------------------------------------------------+ > M-Bus Data link layer
| Stop Character [0x16] | /
+----------------------------------------------------+ -
Data_Length = L - C - A - CI
Each line (except Data) is one Byte
Possible Values found in publicly available docs:
- C: 0x53/0x73 (SND_UD)
- A: FF (Broadcast)
- CI: 0x00-0x1F/0x60/0x61/0x7C/0x7D
- STSAP: 0x01 (Management Logical Device ID 1 of the meter)
- DTSAP: 0x67 (Consumer Information Push Client ID 103)
*/
// MBUS start bytes for different telegram formats:
// - Single Character: 0xE5 (length=1)
// - Short Frame: 0x10 (length=5)
// - Control Frame: 0x68 (length=9)
// - Long Frame: 0x68 (length=9+data_length)
// This component currently only uses Long Frame.
static constexpr uint8_t START_BYTE_SINGLE_CHARACTER = 0xE5;
static constexpr uint8_t START_BYTE_SHORT_FRAME = 0x10;
static constexpr uint8_t START_BYTE_CONTROL_FRAME = 0x68;
static constexpr uint8_t START_BYTE_LONG_FRAME = 0x68;
static constexpr uint8_t MBUS_HEADER_INTRO_LENGTH = 4; // Header length for the intro (0x68, length, length, 0x68)
static constexpr uint8_t MBUS_FULL_HEADER_LENGTH = 9; // Total header length
static constexpr uint8_t MBUS_FOOTER_LENGTH = 2; // Footer after frame
static constexpr uint8_t MBUS_MAX_FRAME_LENGTH = 250; // Maximum size of frame
static constexpr uint8_t MBUS_START1_OFFSET = 0; // Offset of first start byte
static constexpr uint8_t MBUS_LENGTH1_OFFSET = 1; // Offset of first length byte
static constexpr uint8_t MBUS_LENGTH2_OFFSET = 2; // Offset of (duplicated) second length byte
static constexpr uint8_t MBUS_START2_OFFSET = 3; // Offset of (duplicated) second start byte
static constexpr uint8_t STOP_BYTE = 0x16;
} // namespace esphome::dlms_meter

View File

@@ -1,94 +0,0 @@
#pragma once
#include <cstdint>
namespace esphome::dlms_meter {
// Data types as per specification
enum DataType {
NULL_DATA = 0x00,
BOOLEAN = 0x03,
BIT_STRING = 0x04,
DOUBLE_LONG = 0x05,
DOUBLE_LONG_UNSIGNED = 0x06,
OCTET_STRING = 0x09,
VISIBLE_STRING = 0x0A,
UTF8_STRING = 0x0C,
BINARY_CODED_DECIMAL = 0x0D,
INTEGER = 0x0F,
LONG = 0x10,
UNSIGNED = 0x11,
LONG_UNSIGNED = 0x12,
LONG64 = 0x14,
LONG64_UNSIGNED = 0x15,
ENUM = 0x16,
FLOAT32 = 0x17,
FLOAT64 = 0x18,
DATE_TIME = 0x19,
DATE = 0x1A,
TIME = 0x1B,
ARRAY = 0x01,
STRUCTURE = 0x02,
COMPACT_ARRAY = 0x13
};
enum Medium {
ABSTRACT = 0x00,
ELECTRICITY = 0x01,
HEAT_COST_ALLOCATOR = 0x04,
COOLING = 0x05,
HEAT = 0x06,
GAS = 0x07,
COLD_WATER = 0x08,
HOT_WATER = 0x09,
OIL = 0x10,
COMPRESSED_AIR = 0x11,
NITROGEN = 0x12
};
// Data structure
static constexpr uint8_t DECODER_START_OFFSET = 20; // Skip header, timestamp and break block
static constexpr uint8_t OBIS_TYPE_OFFSET = 0;
static constexpr uint8_t OBIS_LENGTH_OFFSET = 1;
static constexpr uint8_t OBIS_CODE_OFFSET = 2;
static constexpr uint8_t OBIS_CODE_LENGTH_STANDARD = 0x06; // 6-byte OBIS code (A.B.C.D.E.F)
static constexpr uint8_t OBIS_CODE_LENGTH_EXTENDED = 0x0C; // 12-byte extended OBIS code
static constexpr uint8_t OBIS_A = 0;
static constexpr uint8_t OBIS_B = 1;
static constexpr uint8_t OBIS_C = 2;
static constexpr uint8_t OBIS_D = 3;
static constexpr uint8_t OBIS_E = 4;
static constexpr uint8_t OBIS_F = 5;
// Metadata
static constexpr uint16_t OBIS_TIMESTAMP = 0x0100;
static constexpr uint16_t OBIS_SERIAL_NUMBER = 0x6001;
static constexpr uint16_t OBIS_DEVICE_NAME = 0x2A00;
// Voltage
static constexpr uint16_t OBIS_VOLTAGE_L1 = 0x2007;
static constexpr uint16_t OBIS_VOLTAGE_L2 = 0x3407;
static constexpr uint16_t OBIS_VOLTAGE_L3 = 0x4807;
// Current
static constexpr uint16_t OBIS_CURRENT_L1 = 0x1F07;
static constexpr uint16_t OBIS_CURRENT_L2 = 0x3307;
static constexpr uint16_t OBIS_CURRENT_L3 = 0x4707;
// Power
static constexpr uint16_t OBIS_ACTIVE_POWER_PLUS = 0x0107;
static constexpr uint16_t OBIS_ACTIVE_POWER_MINUS = 0x0207;
// Active energy
static constexpr uint16_t OBIS_ACTIVE_ENERGY_PLUS = 0x0108;
static constexpr uint16_t OBIS_ACTIVE_ENERGY_MINUS = 0x0208;
// Reactive energy
static constexpr uint16_t OBIS_REACTIVE_ENERGY_PLUS = 0x0308;
static constexpr uint16_t OBIS_REACTIVE_ENERGY_MINUS = 0x0408;
// Netz NOE specific
static constexpr uint16_t OBIS_POWER_FACTOR = 0x0D07;
} // namespace esphome::dlms_meter

View File

@@ -1,124 +0,0 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_VOLT,
UNIT_WATT,
UNIT_WATT_HOURS,
)
from .. import CONF_DLMS_METER_ID, DlmsMeterComponent
AUTO_LOAD = ["dlms_meter"]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
cv.Optional("voltage_l1"): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("voltage_l2"): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("voltage_l3"): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("current_l1"): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("current_l2"): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("current_l3"): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_power_plus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_power_minus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_energy_plus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("active_energy_minus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_plus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_minus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
# Netz NOE
cv.Optional("power_factor"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
sensors = []
for key, conf in config.items():
if not isinstance(conf, dict):
continue
id = conf[CONF_ID]
if id and id.type == sensor.Sensor:
sens = await sensor.new_sensor(conf)
cg.add(getattr(hub, f"set_{key}_sensor")(sens))
sensors.append(f"F({key})")
if sensors:
cg.add_define(
"DLMS_METER_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors))
)

View File

@@ -1,37 +0,0 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID
from .. import CONF_DLMS_METER_ID, DlmsMeterComponent
AUTO_LOAD = ["dlms_meter"]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
# Netz NOE
cv.Optional("meternumber"): text_sensor.text_sensor_schema(),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
text_sensors = []
for key, conf in config.items():
if not isinstance(conf, dict):
continue
id = conf[CONF_ID]
if id and id.type == text_sensor.TextSensor:
sens = await text_sensor.new_text_sensor(conf)
cg.add(getattr(hub, f"set_{key}_text_sensor")(sens))
text_sensors.append(f"F({key})")
if text_sensors:
cg.add_define(
"DLMS_METER_TEXT_SENSOR_LIST(F, sep)",
cg.RawExpression(" sep ".join(text_sensors)),
)

View File

@@ -739,10 +739,9 @@ CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
CONF_DISABLE_FATFS = "disable_fatfs" CONF_DISABLE_FATFS = "disable_fatfs"
# VFS requirement tracking # VFS requirement tracking
# Components that need VFS features can call require_vfs_*() functions # Components that need VFS features can call require_vfs_select() or require_vfs_dir()
KEY_VFS_SELECT_REQUIRED = "vfs_select_required" KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
KEY_VFS_DIR_REQUIRED = "vfs_dir_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 # Feature requirement tracking - components can call require_* functions to re-enable
# These are stored in CORE.data[KEY_ESP32] dict # These are stored in CORE.data[KEY_ESP32] dict
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required" KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
@@ -769,15 +768,6 @@ def require_vfs_dir() -> None:
CORE.data[KEY_VFS_DIR_REQUIRED] = True 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: def require_full_certificate_bundle() -> None:
"""Request the full certificate bundle instead of the common-CAs-only bundle. """Request the full certificate bundle instead of the common-CAs-only bundle.
@@ -1287,18 +1277,6 @@ async def to_code(config):
# Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms # Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms
add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000) add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000)
# Reduce FreeRTOS max priorities from 25 to 16 to save RAM
# pxReadyTasksLists uses 20 bytes per priority level, so this saves 180 bytes
# All ESPHome tasks use relative priorities (configMAX_PRIORITIES - X) to scale automatically
# See https://github.com/espressif/esp-idf/issues/13041 for context
add_idf_sdkconfig_option("CONFIG_FREERTOS_MAX_PRIORITIES", 16)
# Set LWIP TCP/IP task priority to fit within reduced priority range (0-15)
# Default is 18, which would be invalid with MAX_PRIORITIES=16
# Priority 8 maintains the original hierarchy: I2S speaker (10) > LWIP (8) > mixer (6)
# This ensures audio I/O tasks aren't blocked by network, while network isn't starved by mixing
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_TASK_PRIO", 8)
# Place non-ISR FreeRTOS functions into flash instead of IRAM # Place non-ISR FreeRTOS functions into flash instead of IRAM
# This saves up to 8KB of IRAM. ISR-safe functions (FromISR variants) stay in IRAM. # This saves up to 8KB of IRAM. ISR-safe functions (FromISR variants) stay in IRAM.
# In ESP-IDF 6.0 this becomes the default and CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH # In ESP-IDF 6.0 this becomes the default and CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH
@@ -1341,10 +1319,6 @@ async def to_code(config):
# Disable dynamic log level control to save memory # Disable dynamic log level control to save memory
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) 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 # Reduce PHY TX power in the event of a brownout
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
@@ -1398,18 +1372,11 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
# Disable VFS support for termios (terminal I/O functions) # Disable VFS support for termios (terminal I/O functions)
# USB Serial JTAG VFS functions require termios support. # ESPHome doesn't use termios functions on ESP32 (only used in host UART driver).
# 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). # Saves approximately 1.8KB of flash when disabled (default).
if CORE.data.get(KEY_VFS_TERMIOS_REQUIRED, False): add_idf_sdkconfig_option(
# Component requires VFS termios - force enable regardless of user setting "CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS]
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 # Disable VFS support for select() with file descriptors
# ESPHome only uses select() with sockets via lwip_select(), which still works. # ESPHome only uses select() with sockets via lwip_select(), which still works.

View File

@@ -3,7 +3,6 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/task_priorities.h"
#include "preferences.h" #include "preferences.h"
#include <esp_clk_tree.h> #include <esp_clk_tree.h>
#include <esp_cpu.h> #include <esp_cpu.h>
@@ -67,14 +66,10 @@ void loop_task(void *pv_params) {
extern "C" void app_main() { extern "C" void app_main() {
initArduino(); initArduino();
esp32::setup_preferences(); esp32::setup_preferences();
// TASK_PRIORITY_APPLICATION: baseline priority for main loop - all component loops
// run here. Higher priority tasks (audio, network) preempt this when needed.
#if CONFIG_FREERTOS_UNICORE #if CONFIG_FREERTOS_UNICORE
xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, TASK_PRIORITY_APPLICATION, xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle);
&loop_task_handle);
#else #else
xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, TASK_PRIORITY_APPLICATION, xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1);
&loop_task_handle, 1);
#endif #endif
} }

View File

@@ -4,7 +4,6 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include <freertos/task.h> #include <freertos/task.h>
@@ -43,13 +42,11 @@ void ESP32Camera::setup() {
/* initialize RTOS */ /* initialize RTOS */
this->framebuffer_get_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); this->framebuffer_get_queue_ = xQueueCreate(1, sizeof(camera_fb_t *));
this->framebuffer_return_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); this->framebuffer_return_queue_ = xQueueCreate(1, sizeof(camera_fb_t *));
// TASK_PRIORITY_APPLICATION: same as main loop - camera capture is buffered,
// not real-time critical like audio
xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task, xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task,
"framebuffer_task", // name "framebuffer_task", // name
FRAMEBUFFER_TASK_STACK_SIZE, // stack size FRAMEBUFFER_TASK_STACK_SIZE, // stack size
this, // task pv params this, // task pv params
TASK_PRIORITY_APPLICATION, // priority 1, // priority
nullptr, // handle nullptr, // handle
1 // core 1 // core
); );

View File

@@ -2,9 +2,6 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/version.h" #include "esphome/core/version.h"
#ifdef USE_ESP32
#include "esphome/core/task_priorities.h"
#endif
#include "esphome/components/json/json_util.h" #include "esphome/components/json/json_util.h"
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
@@ -49,9 +46,7 @@ void HttpRequestUpdate::update() {
return; return;
} }
#ifdef USE_ESP32 #ifdef USE_ESP32
// TASK_PRIORITY_APPLICATION: same as main loop - update check is background work xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_);
xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, TASK_PRIORITY_APPLICATION,
&this->update_task_handle_);
#else #else
this->update_task(this); this->update_task(this);
#endif #endif

View File

@@ -11,6 +11,12 @@ namespace i2c {
static const char *const TAG = "i2c"; static const char *const TAG = "i2c";
void I2CBus::i2c_scan_() { 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++) { for (uint8_t address = 8; address != 120; address++) {
auto err = write_readv(address, nullptr, 0, nullptr, 0); auto err = write_readv(address, nullptr, 0, nullptr, 0);
if (err == ERROR_OK) { if (err == ERROR_OK) {
@@ -21,6 +27,9 @@ void I2CBus::i2c_scan_() {
// it takes 16sec to scan on nrf52. It prevents board reset. // it takes 16sec to scan on nrf52. It prevents board reset.
arch_feed_wdt(); 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) { ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len) {

View File

@@ -11,7 +11,6 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include "esphome/components/audio/audio.h" #include "esphome/components/audio/audio.h"
@@ -23,6 +22,7 @@ static const UBaseType_t MAX_LISTENERS = 16;
static const uint32_t READ_DURATION_MS = 16; static const uint32_t READ_DURATION_MS = 16;
static const size_t TASK_STACK_SIZE = 4096; static const size_t TASK_STACK_SIZE = 4096;
static const ssize_t TASK_PRIORITY = 23;
static const char *const TAG = "i2s_audio.microphone"; static const char *const TAG = "i2s_audio.microphone";
@@ -520,10 +520,8 @@ void I2SAudioMicrophone::loop() {
} }
if (this->task_handle_ == nullptr) { if (this->task_handle_ == nullptr) {
// TASK_PRIORITY_AUDIO_CAPTURE: highest application priority - real-time audio xTaskCreate(I2SAudioMicrophone::mic_task, "mic_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
// input cannot tolerate delays without dropping samples &this->task_handle_);
xTaskCreate(I2SAudioMicrophone::mic_task, "mic_task", TASK_STACK_SIZE, (void *) this,
TASK_PRIORITY_AUDIO_CAPTURE, &this->task_handle_);
if (this->task_handle_ == nullptr) { if (this->task_handle_ == nullptr) {
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");

View File

@@ -14,7 +14,6 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include "esp_timer.h" #include "esp_timer.h"
@@ -25,6 +24,7 @@ static const uint32_t DMA_BUFFER_DURATION_MS = 15;
static const size_t DMA_BUFFERS_COUNT = 4; static const size_t DMA_BUFFERS_COUNT = 4;
static const size_t TASK_STACK_SIZE = 4096; static const size_t TASK_STACK_SIZE = 4096;
static const ssize_t TASK_PRIORITY = 19;
static const size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1; static const size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1;
@@ -151,10 +151,8 @@ void I2SAudioSpeaker::loop() {
} }
if (this->speaker_task_handle_ == nullptr) { if (this->speaker_task_handle_ == nullptr) {
// TASK_PRIORITY_AUDIO_OUTPUT: high priority for real-time audio output, xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
// below capture (TASK_PRIORITY_AUDIO_CAPTURE) but above network tasks &this->speaker_task_handle_);
xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this,
TASK_PRIORITY_AUDIO_OUTPUT, &this->speaker_task_handle_);
if (this->speaker_task_handle_ == nullptr) { if (this->speaker_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");

View File

@@ -15,7 +15,7 @@ static const char *const TAG = "json";
static SpiRamAllocator global_json_allocator; static SpiRamAllocator global_json_allocator;
#endif #endif
std::string build_json(const json_build_t &f) { SerializationBuffer<> build_json(const json_build_t &f) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonBuilder builder; JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -61,14 +61,62 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
std::string JsonBuilder::serialize() { SerializationBuffer<> JsonBuilder::serialize() {
// ===========================================================================================
// CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING
// ===========================================================================================
//
// This function is carefully structured to enable NRVO. The compiler constructs `result`
// directly in the caller's stack frame, eliminating the move constructor call entirely.
//
// WITHOUT NRVO: Each return would trigger SerializationBuffer's move constructor, which
// must memcpy up to 512 bytes of stack buffer content. This happens on EVERY JSON
// serialization (sensor updates, web server responses, MQTT publishes, etc.).
//
// WITH NRVO: Zero memcpy, zero move constructor overhead. The buffer lives directly
// where the caller needs it.
//
// Requirements for NRVO to work:
// 1. Single named variable (`result`) returned from ALL paths
// 2. All paths must return the SAME variable (not different variables)
// 3. No std::move() on the return statement
//
// If you must modify this function:
// - Keep a single `result` variable declared at the top
// - All code paths must return `result` (not a different variable)
// - Verify NRVO still works by checking the disassembly for move constructor calls
// - Test: objdump -d -C firmware.elf | grep "SerializationBuffer.*SerializationBuffer"
// Should show only destructor, NOT move constructor
//
// Why we avoid measureJson(): It instantiates DummyWriter templates adding ~1KB flash.
// Instead, try stack buffer first. 512 bytes covers 99.9% of JSON payloads (sensors ~200B,
// lights ~170B, climate ~700B). Only entities with 40+ options exceed this.
//
// ===========================================================================================
constexpr size_t buf_size = SerializationBuffer<>::BUFFER_SIZE;
SerializationBuffer<> result(buf_size - 1); // Max content size (reserve 1 for null)
if (doc_.overflowed()) { if (doc_.overflowed()) {
ESP_LOGE(TAG, "JSON document overflow"); ESP_LOGE(TAG, "JSON document overflow");
return "{}"; auto *buf = result.data_writable_();
buf[0] = '{';
buf[1] = '}';
buf[2] = '\0';
result.set_size_(2);
return result;
} }
std::string output;
serializeJson(doc_, output); size_t size = serializeJson(doc_, result.data_writable_(), buf_size);
return output; if (size < buf_size) {
// Fits in stack buffer - update size to actual length
result.set_size_(size);
return result;
}
// Needs heap allocation - reallocate and serialize again with exact size
result.reallocate_heap_(size);
serializeJson(doc_, result.data_writable_(), size + 1);
return result;
} }
} // namespace json } // namespace json

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <cstring>
#include <string>
#include <vector> #include <vector>
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
@@ -14,6 +16,108 @@
namespace esphome { namespace esphome {
namespace json { namespace json {
/// Buffer for JSON serialization that uses stack allocation for small payloads.
/// Template parameter STACK_SIZE specifies the stack buffer size (default 512 bytes).
/// Supports move semantics for efficient return-by-value.
template<size_t STACK_SIZE = 512> class SerializationBuffer {
public:
static constexpr size_t BUFFER_SIZE = STACK_SIZE; ///< Stack buffer size for this instantiation
/// Construct with known size (typically from measureJson)
explicit SerializationBuffer(size_t size) : size_(size) {
if (size + 1 <= STACK_SIZE) {
buffer_ = stack_buffer_;
} else {
heap_buffer_ = new char[size + 1];
buffer_ = heap_buffer_;
}
buffer_[0] = '\0';
}
~SerializationBuffer() { delete[] heap_buffer_; }
// Move constructor - works with same template instantiation
SerializationBuffer(SerializationBuffer &&other) noexcept : heap_buffer_(other.heap_buffer_), size_(other.size_) {
if (other.buffer_ == other.stack_buffer_) {
// Stack buffer - must copy content
std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1);
buffer_ = stack_buffer_;
} else {
// Heap buffer - steal ownership
buffer_ = heap_buffer_;
other.heap_buffer_ = nullptr;
}
// Leave moved-from object in valid empty state
other.stack_buffer_[0] = '\0';
other.buffer_ = other.stack_buffer_;
other.size_ = 0;
}
// Move assignment
SerializationBuffer &operator=(SerializationBuffer &&other) noexcept {
if (this != &other) {
delete[] heap_buffer_;
heap_buffer_ = other.heap_buffer_;
size_ = other.size_;
if (other.buffer_ == other.stack_buffer_) {
std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1);
buffer_ = stack_buffer_;
} else {
buffer_ = heap_buffer_;
other.heap_buffer_ = nullptr;
}
// Leave moved-from object in valid empty state
other.stack_buffer_[0] = '\0';
other.buffer_ = other.stack_buffer_;
other.size_ = 0;
}
return *this;
}
// Delete copy operations
SerializationBuffer(const SerializationBuffer &) = delete;
SerializationBuffer &operator=(const SerializationBuffer &) = delete;
/// Get null-terminated C string
const char *c_str() const { return buffer_; }
/// Get data pointer
const char *data() const { return buffer_; }
/// Get string length (excluding null terminator)
size_t size() const { return size_; }
/// Implicit conversion to std::string for backward compatibility
/// WARNING: This allocates a new std::string on the heap. Prefer using
/// c_str() or data()/size() directly when possible to avoid allocation.
operator std::string() const { return std::string(buffer_, size_); } // NOLINT(google-explicit-constructor)
private:
friend class JsonBuilder; ///< Allows JsonBuilder::serialize() to call private methods
/// Get writable buffer (for serialization)
char *data_writable_() { return buffer_; }
/// Set actual size after serialization (must not exceed allocated size)
/// Also ensures null termination for c_str() safety
void set_size_(size_t size) {
size_ = size;
buffer_[size] = '\0';
}
/// Reallocate to heap buffer with new size (for when stack buffer is too small)
/// This invalidates any previous buffer content. Used by JsonBuilder::serialize().
void reallocate_heap_(size_t size) {
delete[] heap_buffer_;
heap_buffer_ = new char[size + 1];
buffer_ = heap_buffer_;
size_ = size;
buffer_[0] = '\0';
}
char stack_buffer_[STACK_SIZE];
char *heap_buffer_{nullptr};
char *buffer_;
size_t size_;
};
#ifdef USE_PSRAM #ifdef USE_PSRAM
// Build an allocator for the JSON Library using the RAMAllocator class // Build an allocator for the JSON Library using the RAMAllocator class
// This is only compiled when PSRAM is enabled // This is only compiled when PSRAM is enabled
@@ -46,7 +150,8 @@ using json_parse_t = std::function<bool(JsonObject)>;
using json_build_t = std::function<void(JsonObject)>; using json_build_t = std::function<void(JsonObject)>;
/// Build a JSON string with the provided json build function. /// Build a JSON string with the provided json build function.
std::string build_json(const json_build_t &f); /// Returns SerializationBuffer for stack-first allocation; implicitly converts to std::string.
SerializationBuffer<> build_json(const json_build_t &f);
/// Parse a JSON string and run the provided json parse function if it's valid. /// Parse a JSON string and run the provided json parse function if it's valid.
bool parse_json(const std::string &data, const json_parse_t &f); bool parse_json(const std::string &data, const json_parse_t &f);
@@ -69,7 +174,9 @@ class JsonBuilder {
return root_; return root_;
} }
std::string serialize(); /// Serialize the JSON document to a SerializationBuffer (stack-first allocation)
/// Uses 512-byte stack buffer by default, falls back to heap for larger JSON
SerializationBuffer<> serialize();
private: private:
#ifdef USE_PSRAM #ifdef USE_PSRAM

View File

@@ -16,8 +16,6 @@ from esphome.components.esp32 import (
VARIANT_ESP32S3, VARIANT_ESP32S3,
add_idf_sdkconfig_option, add_idf_sdkconfig_option,
get_esp32_variant, 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 import get_libretiny_component, get_libretiny_family
from esphome.components.libretiny.const import ( from esphome.components.libretiny.const import (
@@ -399,15 +397,9 @@ async def to_code(config):
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG") cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG")
# Define platform support flags for components that need auto-detection
try: try:
uart_selection(USB_SERIAL_JTAG) uart_selection(USB_SERIAL_JTAG)
cg.add_define("USE_LOGGER_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: except cv.Invalid:
pass pass
try: try:

View File

@@ -114,6 +114,9 @@ void Logger::pre_setup() {
global_logger = this; global_logger = this;
esp_log_set_vprintf(esp_idf_log_vprintf_); 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"); ESP_LOGI(TAG, "Log initialized");
} }

View File

@@ -28,10 +28,11 @@ CONFIG_SCHEMA = (
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID], config[CONF_NUM_CHIPS]) var = cg.new_Pvariable(config[CONF_ID])
await spi.register_spi_device(var, config, write_only=True) await spi.register_spi_device(var, config, write_only=True)
await display.register_display(var, config) await display.register_display(var, config)
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
cg.add(var.set_intensity(config[CONF_INTENSITY])) cg.add(var.set_intensity(config[CONF_INTENSITY]))
cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE])) cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE]))

View File

@@ -3,7 +3,8 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::max7219 { namespace esphome {
namespace max7219 {
static const char *const TAG = "max7219"; static const char *const TAG = "max7219";
@@ -114,14 +115,12 @@ const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = {
}; };
float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; }
MAX7219Component::MAX7219Component(uint8_t num_chips) : num_chips_(num_chips) {
this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT
memset(this->buffer_, 0, this->num_chips_ * 8);
}
void MAX7219Component::setup() { void MAX7219Component::setup() {
this->spi_setup(); this->spi_setup();
this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT
for (uint8_t i = 0; i < this->num_chips_ * 8; i++)
this->buffer_[i] = 0;
// let's assume the user has all 8 digits connected, only important in daisy chained setups anyway // let's assume the user has all 8 digits connected, only important in daisy chained setups anyway
this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7); this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7);
// let's use our own ASCII -> led pattern encoding // let's use our own ASCII -> led pattern encoding
@@ -230,6 +229,7 @@ void MAX7219Component::set_intensity(uint8_t intensity) {
this->intensity_ = intensity; this->intensity_ = intensity;
} }
} }
void MAX7219Component::set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; }
uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) { uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) {
char buffer[64]; char buffer[64];
@@ -240,4 +240,5 @@ uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time
} }
uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); }
} // namespace esphome::max7219 } // namespace max7219
} // namespace esphome

View File

@@ -6,7 +6,8 @@
#include "esphome/components/spi/spi.h" #include "esphome/components/spi/spi.h"
#include "esphome/components/display/display.h" #include "esphome/components/display/display.h"
namespace esphome::max7219 { namespace esphome {
namespace max7219 {
class MAX7219Component; class MAX7219Component;
@@ -16,8 +17,6 @@ class MAX7219Component : public PollingComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> { spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
public: public:
explicit MAX7219Component(uint8_t num_chips);
void set_writer(max7219_writer_t &&writer); void set_writer(max7219_writer_t &&writer);
void setup() override; void setup() override;
@@ -31,6 +30,7 @@ class MAX7219Component : public PollingComponent,
void display(); void display();
void set_intensity(uint8_t intensity); void set_intensity(uint8_t intensity);
void set_num_chips(uint8_t num_chips);
void set_reverse(bool reverse) { this->reverse_ = reverse; }; void set_reverse(bool reverse) { this->reverse_ = reverse; };
/// Evaluate the printf-format and print the result at the given position. /// Evaluate the printf-format and print the result at the given position.
@@ -56,9 +56,10 @@ class MAX7219Component : public PollingComponent,
uint8_t intensity_{15}; // Intensity of the display from 0 to 15 (most) uint8_t intensity_{15}; // Intensity of the display from 0 to 15 (most)
bool intensity_changed_{}; // True if we need to re-send the intensity bool intensity_changed_{}; // True if we need to re-send the intensity
uint8_t num_chips_{1}; uint8_t num_chips_{1};
uint8_t *buffer_{nullptr}; uint8_t *buffer_;
bool reverse_{false}; bool reverse_{false};
max7219_writer_t writer_{}; max7219_writer_t writer_{};
}; };
} // namespace esphome::max7219 } // namespace max7219
} // namespace esphome

View File

@@ -6,7 +6,6 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include "esphome/components/audio/audio_transfer_buffer.h" #include "esphome/components/audio/audio_transfer_buffer.h"
@@ -26,6 +25,7 @@ static const size_t DATA_TIMEOUT_MS = 50;
static const uint32_t RING_BUFFER_DURATION_MS = 120; static const uint32_t RING_BUFFER_DURATION_MS = 120;
static const uint32_t INFERENCE_TASK_STACK_SIZE = 3072; static const uint32_t INFERENCE_TASK_STACK_SIZE = 3072;
static const UBaseType_t INFERENCE_TASK_PRIORITY = 3;
enum EventGroupBits : uint32_t { enum EventGroupBits : uint32_t {
COMMAND_STOP = (1 << 0), // Signals the inference task should stop COMMAND_STOP = (1 << 0), // Signals the inference task should stop
@@ -305,10 +305,8 @@ void MicroWakeWord::loop() {
return; return;
} }
// TASK_PRIORITY_INFERENCE: above main loop (TASK_PRIORITY_APPLICATION) but below
// protocol tasks (TASK_PRIORITY_PROTOCOL) - ML inference is background work
xTaskCreate(MicroWakeWord::inference_task, "mww", INFERENCE_TASK_STACK_SIZE, (void *) this, xTaskCreate(MicroWakeWord::inference_task, "mww", INFERENCE_TASK_STACK_SIZE, (void *) this,
TASK_PRIORITY_INFERENCE, &this->inference_task_handle_); INFERENCE_TASK_PRIORITY, &this->inference_task_handle_);
if (this->inference_task_handle_ == nullptr) { if (this->inference_task_handle_ == nullptr) {
FrontendFreeStateContents(&this->frontend_state_); // Deallocate frontend state FrontendFreeStateContents(&this->frontend_state_); // Deallocate frontend state

View File

@@ -1,39 +1,6 @@
#include "mipi_spi.h" #include "mipi_spi.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::mipi_spi { namespace esphome {
namespace mipi_spi {} // namespace mipi_spi
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl, } // namespace esphome
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,11 +63,6 @@ enum BusType {
BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer 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. * 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. * All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file.
@@ -206,9 +201,37 @@ class MipiSpi : public display::Display,
} }
void dump_config() override { void dump_config() override {
internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_, esph_log_config(TAG,
DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_, "MIPI_SPI Display\n"
this->mode_, this->data_rate_, BUS_TYPE); " 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);
} }
protected: protected:

View File

@@ -5,7 +5,6 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
@@ -13,6 +12,8 @@
namespace esphome { namespace esphome {
namespace mixer_speaker { namespace mixer_speaker {
static const UBaseType_t MIXER_TASK_PRIORITY = 10;
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50; static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
static const uint32_t TASK_DELAY_MS = 25; static const uint32_t TASK_DELAY_MS = 25;
@@ -384,10 +385,8 @@ esp_err_t MixerSpeaker::start_task_() {
} }
if (this->task_handle_ == nullptr) { if (this->task_handle_ == nullptr) {
// TASK_PRIORITY_AUDIO_MIXER: below I2S tasks (TASK_PRIORITY_AUDIO_OUTPUT) but
// above protocol tasks - mixing is buffered but feeds real-time output
this->task_handle_ = xTaskCreateStatic(audio_mixer_task, "mixer", TASK_STACK_SIZE, (void *) this, this->task_handle_ = xTaskCreateStatic(audio_mixer_task, "mixer", TASK_STACK_SIZE, (void *) this,
TASK_PRIORITY_AUDIO_MIXER, this->task_stack_buffer_, &this->task_stack_); MIXER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_);
} }
if (this->task_handle_ == nullptr) { if (this->task_handle_ == nullptr) {

View File

@@ -61,10 +61,7 @@ bool MQTTBackendESP32::initialize_() {
// Create the task only after MQTT client is initialized successfully // Create the task only after MQTT client is initialized successfully
// Use larger stack size when TLS is enabled // Use larger stack size when TLS is enabled
size_t stack_size = this->ca_certificate_.has_value() ? TASK_STACK_SIZE_TLS : TASK_STACK_SIZE; size_t stack_size = this->ca_certificate_.has_value() ? TASK_STACK_SIZE_TLS : TASK_STACK_SIZE;
// TASK_PRIORITY_PROTOCOL: above main loop (TASK_PRIORITY_APPLICATION) but below xTaskCreate(esphome_mqtt_task, "esphome_mqtt", stack_size, (void *) this, TASK_PRIORITY, &this->task_handle_);
// audio tasks - MQTT needs responsive scheduling for message handling
xTaskCreate(esphome_mqtt_task, "esphome_mqtt", stack_size, (void *) this, TASK_PRIORITY_PROTOCOL,
&this->task_handle_);
if (this->task_handle_ == nullptr) { if (this->task_handle_ == nullptr) {
ESP_LOGE(TAG, "Failed to create MQTT task"); ESP_LOGE(TAG, "Failed to create MQTT task");
// Clean up MQTT client since we can't start the async task // Clean up MQTT client since we can't start the async task

View File

@@ -14,7 +14,6 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/lock_free_queue.h" #include "esphome/core/lock_free_queue.h"
#include "esphome/core/event_pool.h" #include "esphome/core/event_pool.h"
#include "esphome/core/task_priorities.h"
namespace esphome::mqtt { namespace esphome::mqtt {
@@ -118,7 +117,8 @@ class MQTTBackendESP32 final : public MQTTBackend {
static const size_t MQTT_BUFFER_SIZE = 4096; static const size_t MQTT_BUFFER_SIZE = 4096;
static const size_t TASK_STACK_SIZE = 3072; static const size_t TASK_STACK_SIZE = 3072;
static const size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations static const size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations
static const uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 static const ssize_t TASK_PRIORITY = 5;
static const uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360
void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; }
void set_client_id(const char *client_id) final { this->client_id_ = client_id; } void set_client_id(const char *client_id) final { this->client_id_ = client_id; }

View File

@@ -564,8 +564,8 @@ bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t
} }
bool MQTTClientComponent::publish_json(const char *topic, const json::json_build_t &f, uint8_t qos, bool retain) { bool MQTTClientComponent::publish_json(const char *topic, const json::json_build_t &f, uint8_t qos, bool retain) {
std::string message = json::build_json(f); auto message = json::build_json(f);
return this->publish(topic, message.c_str(), message.length(), qos, retain); return this->publish(topic, message.c_str(), message.size(), qos, retain);
} }
void MQTTClientComponent::enable() { void MQTTClientComponent::enable() {
@@ -643,34 +643,10 @@ static bool topic_match(const char *message, const char *subscription) {
} }
void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) { void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) {
#ifdef USE_ESP8266 for (auto &subscription : this->subscriptions_) {
// IMPORTANT: This defer is REQUIRED to prevent stack overflow crashes on ESP8266. if (topic_match(topic.c_str(), subscription.topic.c_str()))
// subscription.callback(topic, payload);
// 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 // Setters

View File

@@ -10,7 +10,6 @@
#include "esp_task_wdt.h" #include "esp_task_wdt.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include "esp_err.h" #include "esp_err.h"
#include "esp_event.h" #include "esp_event.h"
@@ -40,14 +39,12 @@ void OpenThreadComponent::setup() {
ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_vfs_eventfd_register(&eventfd_config)); ESP_ERROR_CHECK(esp_vfs_eventfd_register(&eventfd_config));
// TASK_PRIORITY_PROTOCOL: same as USB host/MQTT - network protocol tasks need
// responsive scheduling but below audio tasks
xTaskCreate( xTaskCreate(
[](void *arg) { [](void *arg) {
static_cast<OpenThreadComponent *>(arg)->ot_main(); static_cast<OpenThreadComponent *>(arg)->ot_main();
vTaskDelete(nullptr); vTaskDelete(nullptr);
}, },
"ot_main", 10240, this, TASK_PRIORITY_PROTOCOL, nullptr); "ot_main", 10240, this, 5, nullptr);
} }
static esp_netif_t *init_openthread_netif(const esp_openthread_platform_config_t *config) { static esp_netif_t *init_openthread_netif(const esp_openthread_platform_config_t *config) {

View File

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

View File

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

View File

@@ -40,128 +40,34 @@ pmsx003_ns = cg.esphome_ns.namespace("pmsx003")
PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Component) PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Component)
PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor) PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor)
TYPE_PMS1003 = "PMS1003" TYPE_PMSX003 = "PMSX003"
TYPE_PMS3003 = "PMS3003"
TYPE_PMSX003 = "PMSX003" # PMS5003, PMS6003, PMS7003, PMSA003 (NOT PMSA003I - see `pmsa003i` component)
TYPE_PMS5003S = "PMS5003S"
TYPE_PMS5003T = "PMS5003T" TYPE_PMS5003T = "PMS5003T"
TYPE_PMS5003ST = "PMS5003ST" TYPE_PMS5003ST = "PMS5003ST"
TYPE_PMS9003M = "PMS9003M" TYPE_PMS5003S = "PMS5003S"
Type = pmsx003_ns.enum("Type", is_class=True) PMSX003Type = pmsx003_ns.enum("PMSX003Type")
PMSX003_TYPES = { PMSX003_TYPES = {
TYPE_PMS1003: Type.PMS1003, TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003,
TYPE_PMS3003: Type.PMS3003, TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T,
TYPE_PMSX003: Type.PMSX003, TYPE_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST,
TYPE_PMS5003S: Type.PMS5003S, TYPE_PMS5003S: PMSX003Type.PMSX003_TYPE_5003S,
TYPE_PMS5003T: Type.PMS5003T,
TYPE_PMS5003ST: Type.PMS5003ST,
TYPE_PMS9003M: Type.PMS9003M,
} }
SENSORS_TO_TYPE = { SENSORS_TO_TYPE = {
CONF_PM_1_0_STD: [ CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
TYPE_PMS1003, CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
TYPE_PMS3003, CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
TYPE_PMSX003, CONF_PM_1_0_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
TYPE_PMS5003S, CONF_PM_2_5_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
TYPE_PMS5003T, CONF_PM_10_0_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
TYPE_PMS5003ST, CONF_PM_0_3UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
TYPE_PMS9003M, 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_5_STD: [ CONF_PM_2_5UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S],
TYPE_PMS1003, CONF_PM_5_0UM: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S],
TYPE_PMS3003, CONF_PM_10_0UM: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S],
TYPE_PMSX003, CONF_FORMALDEHYDE: [TYPE_PMS5003ST, TYPE_PMS5003S],
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_TEMPERATURE: [TYPE_PMS5003T, TYPE_PMS5003ST],
CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST], CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST],
} }

View File

@@ -6,7 +6,6 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
@@ -14,6 +13,8 @@
namespace esphome { namespace esphome {
namespace resampler { namespace resampler {
static const UBaseType_t RESAMPLER_TASK_PRIORITY = 1;
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50; static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
static const uint32_t TASK_DELAY_MS = 20; static const uint32_t TASK_DELAY_MS = 20;
@@ -184,10 +185,8 @@ esp_err_t ResamplerSpeaker::start_task_() {
} }
if (this->task_handle_ == nullptr) { if (this->task_handle_ == nullptr) {
// TASK_PRIORITY_APPLICATION: same as main loop - resampling is buffered audio
// processing, not real-time I/O
this->task_handle_ = xTaskCreateStatic(resample_task, "sample", TASK_STACK_SIZE, (void *) this, this->task_handle_ = xTaskCreateStatic(resample_task, "sample", TASK_STACK_SIZE, (void *) this,
TASK_PRIORITY_APPLICATION, this->task_stack_buffer_, &this->task_stack_); RESAMPLER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_);
} }
if (this->task_handle_ == nullptr) { if (this->task_handle_ == nullptr) {

View File

@@ -3,7 +3,6 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include "esphome/components/audio/audio.h" #include "esphome/components/audio/audio.h"
#ifdef USE_OTA #ifdef USE_OTA
@@ -46,6 +45,9 @@ namespace speaker {
static const uint32_t MEDIA_CONTROLS_QUEUE_LENGTH = 20; static const uint32_t MEDIA_CONTROLS_QUEUE_LENGTH = 20;
static const UBaseType_t MEDIA_PIPELINE_TASK_PRIORITY = 1;
static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1;
static const char *const TAG = "speaker_media_player"; static const char *const TAG = "speaker_media_player";
void SpeakerMediaPlayer::setup() { void SpeakerMediaPlayer::setup() {
@@ -68,10 +70,9 @@ void SpeakerMediaPlayer::setup() {
ota::get_global_ota_callback()->add_global_state_listener(this); ota::get_global_ota_callback()->add_global_state_listener(this);
#endif #endif
// TASK_PRIORITY_APPLICATION: same as main loop - media pipelines handle buffered this->announcement_pipeline_ =
// audio streaming, not real-time I/O, so they don't need elevated priority make_unique<AudioPipeline>(this->announcement_speaker_, this->buffer_size_, this->task_stack_in_psram_, "ann",
this->announcement_pipeline_ = make_unique<AudioPipeline>( ANNOUNCEMENT_PIPELINE_TASK_PRIORITY);
this->announcement_speaker_, this->buffer_size_, this->task_stack_in_psram_, "ann", TASK_PRIORITY_APPLICATION);
if (this->announcement_pipeline_ == nullptr) { if (this->announcement_pipeline_ == nullptr) {
ESP_LOGE(TAG, "Failed to create announcement pipeline"); ESP_LOGE(TAG, "Failed to create announcement pipeline");
@@ -80,7 +81,7 @@ void SpeakerMediaPlayer::setup() {
if (!this->single_pipeline_()) { if (!this->single_pipeline_()) {
this->media_pipeline_ = make_unique<AudioPipeline>(this->media_speaker_, this->buffer_size_, this->media_pipeline_ = make_unique<AudioPipeline>(this->media_speaker_, this->buffer_size_,
this->task_stack_in_psram_, "med", TASK_PRIORITY_APPLICATION); this->task_stack_in_psram_, "med", MEDIA_PIPELINE_TASK_PRIORITY);
if (this->media_pipeline_ == nullptr) { if (this->media_pipeline_ == nullptr) {
ESP_LOGE(TAG, "Failed to create media pipeline"); ESP_LOGE(TAG, "Failed to create media pipeline");

View File

@@ -7,7 +7,6 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/gpio.h" #include "esphome/core/gpio.h"
#include "esphome/core/task_priorities.h"
#include "driver/gpio.h" #include "driver/gpio.h"
#include "soc/gpio_num.h" #include "soc/gpio_num.h"
#include "soc/uart_pins.h" #include "soc/uart_pins.h"
@@ -368,13 +367,12 @@ void IDFUARTComponent::check_logger_conflict() {}
#ifdef USE_UART_WAKE_LOOP_ON_RX #ifdef USE_UART_WAKE_LOOP_ON_RX
void IDFUARTComponent::start_rx_event_task_() { void IDFUARTComponent::start_rx_event_task_() {
// TASK_PRIORITY_APPLICATION: same as main loop - UART RX monitoring is lightweight, // Create FreeRTOS task to monitor UART events
// just wakes main loop when data arrives BaseType_t result = xTaskCreate(rx_event_task_func, // Task function
BaseType_t result = xTaskCreate(rx_event_task_func, // Task function "uart_rx_evt", // Task name (max 16 chars)
"uart_rx_evt", // Task name (max 16 chars) 2240, // Stack size in bytes (~2.2KB); increase if needed for logging
2240, // Stack size in bytes (~2.2KB) this, // Task parameter (this pointer)
this, // Task parameter (this pointer) tskIDLE_PRIORITY + 1, // Priority (low, just above idle)
TASK_PRIORITY_APPLICATION,
&this->rx_event_task_handle_ // Task handle &this->rx_event_task_handle_ // Task handle
); );

View File

@@ -12,8 +12,8 @@ from esphome.components.packet_transport import (
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
from esphome.core import ID from esphome.core import ID, Lambda
from esphome.cpp_generator import literal from esphome.cpp_generator import ExpressionStatement, MockObj
CODEOWNERS = ["@clydebarrow"] CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
@@ -24,8 +24,6 @@ udp_ns = cg.esphome_ns.namespace("udp")
UDPComponent = udp_ns.class_("UDPComponent", cg.Component) UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action) UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
trigger_args = cg.std_vector.template(cg.uint8) trigger_args = cg.std_vector.template(cg.uint8)
trigger_argname = "data"
trigger_argtype = [(trigger_args, trigger_argname)]
CONF_ADDRESSES = "addresses" CONF_ADDRESSES = "addresses"
CONF_LISTEN_ADDRESS = "listen_address" CONF_LISTEN_ADDRESS = "listen_address"
@@ -113,14 +111,13 @@ async def to_code(config):
cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]])) cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]]))
if on_receive := config.get(CONF_ON_RECEIVE): if on_receive := config.get(CONF_ON_RECEIVE):
on_receive = on_receive[0] on_receive = on_receive[0]
trigger_id = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID]) trigger = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID])
trigger = await automation.build_automation( trigger = await automation.build_automation(
trigger_id, trigger_argtype, on_receive trigger, [(trigger_args, "data")], on_receive
) )
trigger_lambda = await cg.process_lambda( trigger = Lambda(str(ExpressionStatement(trigger.trigger(MockObj("data")))))
trigger.trigger(literal(trigger_argname)), trigger_argtype trigger = await cg.process_lambda(trigger, [(trigger_args, "data")])
) cg.add(var.add_listener(trigger))
cg.add(var.add_listener(trigger_lambda))
cg.add(var.set_should_listen()) cg.add(var.set_should_listen())

View File

@@ -2,7 +2,6 @@
#include "usb_cdc_acm.h" #include "usb_cdc_acm.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include <cstring> #include <cstring>
#include <sys/param.h> #include <sys/param.h>
@@ -156,16 +155,13 @@ void USBCDCACMInstance::setup() {
return; return;
} }
// Use a larger stack size for very verbose logging // Use a larger stack size for (very) verbose logging
constexpr size_t stack_size = const size_t stack_size = esp_log_level_get(TAG) > ESP_LOG_DEBUG ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_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 // Create a simple, unique task name per interface
char task_name[] = "usb_tx_0"; char task_name[] = "usb_tx_0";
task_name[sizeof(task_name) - 1] = format_hex_char(static_cast<char>(this->itf_)); task_name[sizeof(task_name) - 1] = format_hex_char(static_cast<char>(this->itf_));
// TASK_PRIORITY_USB_SERIAL: above main loop (TASK_PRIORITY_APPLICATION) and xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, 4, &this->usb_tx_task_handle_);
// wake word (TASK_PRIORITY_INFERENCE), below protocol tasks (TASK_PRIORITY_PROTOCOL)
xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, TASK_PRIORITY_USB_SERIAL, &this->usb_tx_task_handle_);
if (this->usb_tx_task_handle_ == nullptr) { if (this->usb_tx_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Failed to create USB TX task for itf %d", this->itf_); ESP_LOGE(TAG, "Failed to create USB TX task for itf %d", this->itf_);

View File

@@ -4,7 +4,6 @@
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/task_priorities.h"
#include <vector> #include <vector>
#include "usb/usb_host.h" #include "usb/usb_host.h"
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
@@ -70,6 +69,7 @@ static constexpr trq_bitmask_t ALL_REQUESTS_IN_USE = MAX_REQUESTS == 32 ? ~0 : (
static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop
static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples) static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples)
static constexpr UBaseType_t USB_TASK_PRIORITY = 5; // Higher priority than main loop (tskIDLE_PRIORITY + 5)
// used to report a transfer status // used to report a transfer status
struct TransferStatus { struct TransferStatus {

View File

@@ -215,12 +215,11 @@ void USBClient::setup() {
} }
// Create and start USB task // Create and start USB task
// TASK_PRIORITY_PROTOCOL: above main loop (TASK_PRIORITY_APPLICATION) but below
// audio tasks - USB host needs responsive scheduling for device communication
xTaskCreate(usb_task_fn, "usb_task", xTaskCreate(usb_task_fn, "usb_task",
USB_TASK_STACK_SIZE, // Stack size USB_TASK_STACK_SIZE, // Stack size
this, // Task parameter this, // Task parameter
TASK_PRIORITY_PROTOCOL, &this->usb_task_handle_); USB_TASK_PRIORITY, // Priority (higher than main loop)
&this->usb_task_handle_);
if (this->usb_task_handle_ == nullptr) { if (this->usb_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Failed to create USB task"); ESP_LOGE(TAG, "Failed to create USB task");

View File

@@ -209,7 +209,7 @@ void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_
void DeferredUpdateEventSource::process_deferred_queue_() { void DeferredUpdateEventSource::process_deferred_queue_() {
while (!deferred_queue_.empty()) { while (!deferred_queue_.empty()) {
DeferredEvent &de = deferred_queue_.front(); DeferredEvent &de = deferred_queue_.front();
std::string message = de.message_generator_(web_server_, de.source_); auto message = de.message_generator_(web_server_, de.source_);
if (this->send(message.c_str(), "state") != DISCARDED) { if (this->send(message.c_str(), "state") != DISCARDED) {
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
deferred_queue_.erase(deferred_queue_.begin()); deferred_queue_.erase(deferred_queue_.begin());
@@ -266,7 +266,7 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *
// deferred queue still not empty which means downstream event queue full, no point trying to send first // deferred queue still not empty which means downstream event queue full, no point trying to send first
deq_push_back_with_dedup_(source, message_generator); deq_push_back_with_dedup_(source, message_generator);
} else { } else {
std::string message = message_generator(web_server_, source); auto message = message_generator(web_server_, source);
if (this->send(message.c_str(), "state") == DISCARDED) { if (this->send(message.c_str(), "state") == DISCARDED) {
deq_push_back_with_dedup_(source, message_generator); deq_push_back_with_dedup_(source, message_generator);
} else { } else {
@@ -320,7 +320,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource
ws->defer([ws, source]() { ws->defer([ws, source]() {
// Configure reconnect timeout and send config // Configure reconnect timeout and send config
// this should always go through since the AsyncEventSourceClient event queue is empty on connect // this should always go through since the AsyncEventSourceClient event queue is empty on connect
std::string message = ws->get_config_json(); auto message = ws->get_config_json();
source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); source->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
#ifdef USE_WEBSERVER_SORTING #ifdef USE_WEBSERVER_SORTING
@@ -329,10 +329,10 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource
JsonObject root = builder.root(); JsonObject root = builder.root();
root[ESPHOME_F("name")] = group.second.name; root[ESPHOME_F("name")] = group.second.name;
root[ESPHOME_F("sorting_weight")] = group.second.weight; root[ESPHOME_F("sorting_weight")] = group.second.weight;
message = builder.serialize(); auto group_msg = builder.serialize();
// up to 31 groups should be able to be queued initially without defer // up to 31 groups should be able to be queued initially without defer
source->try_send_nodefer(message.c_str(), "sorting_group"); source->try_send_nodefer(group_msg.c_str(), "sorting_group");
} }
#endif #endif
@@ -365,7 +365,7 @@ void WebServer::set_css_include(const char *css_include) { this->css_include_ =
void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; } void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; }
#endif #endif
std::string WebServer::get_config_json() { json::SerializationBuffer<> WebServer::get_config_json() {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -597,20 +597,20 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM
// Note: request->method() is always HTTP_GET here (canHandle ensures this) // Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) { if (entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->sensor_json_(obj, obj->state, detail); auto data = this->sensor_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
} }
request->send(404); request->send(404);
} }
std::string WebServer::sensor_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::sensor_state_json_generator(WebServer *web_server, void *source) {
return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE); return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE);
} }
std::string WebServer::sensor_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::sensor_all_json_generator(WebServer *web_server, void *source) {
return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL); return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL);
} }
std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) { json::SerializationBuffer<> WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -644,23 +644,23 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const
// Note: request->method() is always HTTP_GET here (canHandle ensures this) // Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) { if (entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->text_sensor_json_(obj, obj->state, detail); auto data = this->text_sensor_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
} }
request->send(404); request->send(404);
} }
std::string WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) {
return web_server->text_sensor_json_((text_sensor::TextSensor *) (source), return web_server->text_sensor_json_((text_sensor::TextSensor *) (source),
((text_sensor::TextSensor *) (source))->state, DETAIL_STATE); ((text_sensor::TextSensor *) (source))->state, DETAIL_STATE);
} }
std::string WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) {
return web_server->text_sensor_json_((text_sensor::TextSensor *) (source), return web_server->text_sensor_json_((text_sensor::TextSensor *) (source),
((text_sensor::TextSensor *) (source))->state, DETAIL_ALL); ((text_sensor::TextSensor *) (source))->state, DETAIL_ALL);
} }
std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, json::SerializationBuffer<> WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value,
JsonDetail start_config) { JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -705,7 +705,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->switch_json_(obj, obj->state, detail); auto data = this->switch_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -734,13 +734,13 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
} }
request->send(404); request->send(404);
} }
std::string WebServer::switch_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::switch_state_json_generator(WebServer *web_server, void *source) {
return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE); return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE);
} }
std::string WebServer::switch_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::switch_all_json_generator(WebServer *web_server, void *source) {
return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL); return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL);
} }
std::string WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) { json::SerializationBuffer<> WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -762,7 +762,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
continue; continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->button_json_(obj, detail); auto data = this->button_json_(obj, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
} else if (match.method_equals(ESPHOME_F("press"))) { } else if (match.method_equals(ESPHOME_F("press"))) {
DEFER_ACTION(obj, obj->press()); DEFER_ACTION(obj, obj->press());
@@ -775,10 +775,10 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
} }
request->send(404); request->send(404);
} }
std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::button_all_json_generator(WebServer *web_server, void *source) {
return web_server->button_json_((button::Button *) (source), DETAIL_ALL); return web_server->button_json_((button::Button *) (source), DETAIL_ALL);
} }
std::string WebServer::button_json_(button::Button *obj, JsonDetail start_config) { json::SerializationBuffer<> WebServer::button_json_(button::Button *obj, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -805,22 +805,23 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con
// Note: request->method() is always HTTP_GET here (canHandle ensures this) // Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) { if (entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->binary_sensor_json_(obj, obj->state, detail); auto data = this->binary_sensor_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
} }
request->send(404); request->send(404);
} }
std::string WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) {
return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source), return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE); ((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE);
} }
std::string WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) {
return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source), return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL); ((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL);
} }
std::string WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { json::SerializationBuffer<> WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value,
JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -847,7 +848,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->fan_json_(obj, detail); auto data = this->fan_json_(obj, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
} else if (match.method_equals(ESPHOME_F("toggle"))) { } else if (match.method_equals(ESPHOME_F("toggle"))) {
DEFER_ACTION(obj, obj->toggle().perform()); DEFER_ACTION(obj, obj->toggle().perform());
@@ -888,13 +889,13 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
} }
request->send(404); request->send(404);
} }
std::string WebServer::fan_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::fan_state_json_generator(WebServer *web_server, void *source) {
return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE); return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE);
} }
std::string WebServer::fan_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::fan_all_json_generator(WebServer *web_server, void *source) {
return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL); return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL);
} }
std::string WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) { json::SerializationBuffer<> WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -928,7 +929,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->light_json_(obj, detail); auto data = this->light_json_(obj, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
} else if (match.method_equals(ESPHOME_F("toggle"))) { } else if (match.method_equals(ESPHOME_F("toggle"))) {
DEFER_ACTION(obj, obj->toggle().perform()); DEFER_ACTION(obj, obj->toggle().perform());
@@ -967,13 +968,13 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
} }
request->send(404); request->send(404);
} }
std::string WebServer::light_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::light_state_json_generator(WebServer *web_server, void *source) {
return web_server->light_json_((light::LightState *) (source), DETAIL_STATE); return web_server->light_json_((light::LightState *) (source), DETAIL_STATE);
} }
std::string WebServer::light_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::light_all_json_generator(WebServer *web_server, void *source) {
return web_server->light_json_((light::LightState *) (source), DETAIL_ALL); return web_server->light_json_((light::LightState *) (source), DETAIL_ALL);
} }
std::string WebServer::light_json_(light::LightState *obj, JsonDetail start_config) { json::SerializationBuffer<> WebServer::light_json_(light::LightState *obj, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -1007,7 +1008,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->cover_json_(obj, detail); auto data = this->cover_json_(obj, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -1055,13 +1056,13 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
} }
request->send(404); request->send(404);
} }
std::string WebServer::cover_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::cover_state_json_generator(WebServer *web_server, void *source) {
return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE); return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE);
} }
std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::cover_all_json_generator(WebServer *web_server, void *source) {
return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL); return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL);
} }
std::string WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) { json::SerializationBuffer<> WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -1096,7 +1097,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->number_json_(obj, obj->state, detail); auto data = this->number_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -1115,13 +1116,13 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
request->send(404); request->send(404);
} }
std::string WebServer::number_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::number_state_json_generator(WebServer *web_server, void *source) {
return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE); return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE);
} }
std::string WebServer::number_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::number_all_json_generator(WebServer *web_server, void *source) {
return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL); return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL);
} }
std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) { json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -1163,7 +1164,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
continue; continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->date_json_(obj, detail); auto data = this->date_json_(obj, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -1188,13 +1189,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
request->send(404); request->send(404);
} }
std::string WebServer::date_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::date_state_json_generator(WebServer *web_server, void *source) {
return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE); return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE);
} }
std::string WebServer::date_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::date_all_json_generator(WebServer *web_server, void *source) {
return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL); return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL);
} }
std::string WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) { json::SerializationBuffer<> WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -1223,7 +1224,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
continue; continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->time_json_(obj, detail); auto data = this->time_json_(obj, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -1247,13 +1248,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
} }
request->send(404); request->send(404);
} }
std::string WebServer::time_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::time_state_json_generator(WebServer *web_server, void *source) {
return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE); return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE);
} }
std::string WebServer::time_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::time_all_json_generator(WebServer *web_server, void *source) {
return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL); return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL);
} }
std::string WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) { json::SerializationBuffer<> WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -1282,7 +1283,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
continue; continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->datetime_json_(obj, detail); auto data = this->datetime_json_(obj, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -1306,13 +1307,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
} }
request->send(404); request->send(404);
} }
std::string WebServer::datetime_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::datetime_state_json_generator(WebServer *web_server, void *source) {
return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE); return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE);
} }
std::string WebServer::datetime_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::datetime_all_json_generator(WebServer *web_server, void *source) {
return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL); return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL);
} }
std::string WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) { json::SerializationBuffer<> WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -1343,7 +1344,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->text_json_(obj, obj->state, detail); auto data = this->text_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -1362,13 +1363,13 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
request->send(404); request->send(404);
} }
std::string WebServer::text_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::text_state_json_generator(WebServer *web_server, void *source) {
return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE); return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE);
} }
std::string WebServer::text_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::text_all_json_generator(WebServer *web_server, void *source) {
return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL); return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL);
} }
std::string WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) { json::SerializationBuffer<> WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -1400,7 +1401,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail); auto data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -1419,15 +1420,15 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
} }
request->send(404); request->send(404);
} }
std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::select_state_json_generator(WebServer *web_server, void *source) {
auto *obj = (select::Select *) (source); auto *obj = (select::Select *) (source);
return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_STATE); return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_STATE);
} }
std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::select_all_json_generator(WebServer *web_server, void *source) {
auto *obj = (select::Select *) (source); auto *obj = (select::Select *) (source);
return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_ALL); return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_ALL);
} }
std::string WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) { json::SerializationBuffer<> WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -1459,7 +1460,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->climate_json_(obj, detail); auto data = this->climate_json_(obj, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -1488,15 +1489,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
} }
request->send(404); request->send(404);
} }
std::string WebServer::climate_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::climate_state_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->climate_json_((climate::Climate *) (source), DETAIL_STATE); return web_server->climate_json_((climate::Climate *) (source), DETAIL_STATE);
} }
std::string WebServer::climate_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::climate_all_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->climate_json_((climate::Climate *) (source), DETAIL_ALL); return web_server->climate_json_((climate::Climate *) (source), DETAIL_ALL);
} }
std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) { json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -1629,7 +1630,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->lock_json_(obj, obj->state, detail); auto data = this->lock_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -1658,13 +1659,13 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
} }
request->send(404); request->send(404);
} }
std::string WebServer::lock_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::lock_state_json_generator(WebServer *web_server, void *source) {
return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE); return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE);
} }
std::string WebServer::lock_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::lock_all_json_generator(WebServer *web_server, void *source) {
return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL); return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL);
} }
std::string WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { json::SerializationBuffer<> WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -1692,7 +1693,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->valve_json_(obj, detail); auto data = this->valve_json_(obj, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -1738,13 +1739,13 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
} }
request->send(404); request->send(404);
} }
std::string WebServer::valve_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::valve_state_json_generator(WebServer *web_server, void *source) {
return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE); return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE);
} }
std::string WebServer::valve_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::valve_all_json_generator(WebServer *web_server, void *source) {
return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL); return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL);
} }
std::string WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) { json::SerializationBuffer<> WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -1777,7 +1778,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->alarm_control_panel_json_(obj, obj->get_state(), detail); auto data = this->alarm_control_panel_json_(obj, obj->get_state(), detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -1817,19 +1818,19 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
} }
request->send(404); request->send(404);
} }
std::string WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) {
return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source), return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
DETAIL_STATE); DETAIL_STATE);
} }
std::string WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) {
return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source), return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
DETAIL_ALL); DETAIL_ALL);
} }
std::string WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, json::SerializationBuffer<> WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value, alarm_control_panel::AlarmControlPanelState value,
JsonDetail start_config) { JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -1858,7 +1859,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->water_heater_json_(obj, detail); auto data = this->water_heater_json_(obj, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -1894,14 +1895,14 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
request->send(404); request->send(404);
} }
std::string WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) {
return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_STATE); return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_STATE);
} }
std::string WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_ALL); return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_ALL);
} }
std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) { json::SerializationBuffer<> WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
char buf[PSTR_LOCAL_SIZE]; char buf[PSTR_LOCAL_SIZE];
@@ -1964,7 +1965,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->infrared_json_(obj, detail); auto data = this->infrared_json_(obj, detail);
request->send(200, ESPHOME_F("application/json"), data.c_str()); request->send(200, ESPHOME_F("application/json"), data.c_str());
return; return;
} }
@@ -2035,12 +2036,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
request->send(404); request->send(404);
} }
std::string WebServer::infrared_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::infrared_all_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->infrared_json_(static_cast<infrared::Infrared *>(source), DETAIL_ALL); return web_server->infrared_json_(static_cast<infrared::Infrared *>(source), DETAIL_ALL);
} }
std::string WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) { json::SerializationBuffer<> WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -2075,7 +2076,7 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
// Note: request->method() is always HTTP_GET here (canHandle ensures this) // Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) { if (entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->event_json_(obj, StringRef(), detail); auto data = this->event_json_(obj, StringRef(), detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -2085,16 +2086,16 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
static StringRef get_event_type(event::Event *event) { return event ? event->get_last_event_type() : StringRef(); } static StringRef get_event_type(event::Event *event) { return event ? event->get_last_event_type() : StringRef(); }
std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::event_state_json_generator(WebServer *web_server, void *source) {
auto *event = static_cast<event::Event *>(source); auto *event = static_cast<event::Event *>(source);
return web_server->event_json_(event, get_event_type(event), DETAIL_STATE); return web_server->event_json_(event, get_event_type(event), DETAIL_STATE);
} }
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::event_all_json_generator(WebServer *web_server, void *source) {
auto *event = static_cast<event::Event *>(source); auto *event = static_cast<event::Event *>(source);
return web_server->event_json_(event, get_event_type(event), DETAIL_ALL); return web_server->event_json_(event, get_event_type(event), DETAIL_ALL);
} }
std::string WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) { json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
@@ -2141,7 +2142,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
if (request->method() == HTTP_GET && entity_match.action_is_empty) { if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request); auto detail = get_request_detail(request);
std::string data = this->update_json_(obj, detail); auto data = this->update_json_(obj, detail);
request->send(200, "application/json", data.c_str()); request->send(200, "application/json", data.c_str());
return; return;
} }
@@ -2157,15 +2158,15 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
} }
request->send(404); request->send(404);
} }
std::string WebServer::update_state_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::update_state_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE);
} }
std::string WebServer::update_all_json_generator(WebServer *web_server, void *source) { json::SerializationBuffer<> WebServer::update_all_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE);
} }
std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) { json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();

View File

@@ -2,6 +2,7 @@
#include "list_entities.h" #include "list_entities.h"
#include "esphome/components/json/json_util.h"
#include "esphome/components/web_server_base/web_server_base.h" #include "esphome/components/web_server_base/web_server_base.h"
#ifdef USE_WEBSERVER #ifdef USE_WEBSERVER
#include "esphome/core/component.h" #include "esphome/core/component.h"
@@ -104,7 +105,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE };
can be forgotten. can be forgotten.
*/ */
#if !defined(USE_ESP32) && defined(USE_ARDUINO) #if !defined(USE_ESP32) && defined(USE_ARDUINO)
using message_generator_t = std::string(WebServer *, void *); using message_generator_t = json::SerializationBuffer<>(WebServer *, void *);
class DeferredUpdateEventSourceList; class DeferredUpdateEventSourceList;
class DeferredUpdateEventSource : public AsyncEventSource { class DeferredUpdateEventSource : public AsyncEventSource {
@@ -263,7 +264,7 @@ class WebServer : public Controller,
void handle_index_request(AsyncWebServerRequest *request); void handle_index_request(AsyncWebServerRequest *request);
/// Return the webserver configuration as JSON. /// Return the webserver configuration as JSON.
std::string get_config_json(); json::SerializationBuffer<> get_config_json();
#ifdef USE_WEBSERVER_CSS_INCLUDE #ifdef USE_WEBSERVER_CSS_INCLUDE
/// Handle included css request under '/0.css'. /// Handle included css request under '/0.css'.
@@ -285,8 +286,8 @@ class WebServer : public Controller,
/// Handle a sensor request under '/sensor/<id>'. /// Handle a sensor request under '/sensor/<id>'.
void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string sensor_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> sensor_state_json_generator(WebServer *web_server, void *source);
static std::string sensor_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> sensor_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
@@ -295,8 +296,8 @@ class WebServer : public Controller,
/// Handle a switch request under '/switch/<id>/</turn_on/turn_off/toggle>'. /// Handle a switch request under '/switch/<id>/</turn_on/turn_off/toggle>'.
void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string switch_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> switch_state_json_generator(WebServer *web_server, void *source);
static std::string switch_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> switch_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
@@ -304,7 +305,7 @@ class WebServer : public Controller,
void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match);
// Buttons are stateless, so there is no button_state_json_generator // Buttons are stateless, so there is no button_state_json_generator
static std::string button_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> button_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
@@ -313,8 +314,8 @@ class WebServer : public Controller,
/// Handle a binary sensor request under '/binary_sensor/<id>'. /// Handle a binary sensor request under '/binary_sensor/<id>'.
void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string binary_sensor_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> binary_sensor_state_json_generator(WebServer *web_server, void *source);
static std::string binary_sensor_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> binary_sensor_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
@@ -323,8 +324,8 @@ class WebServer : public Controller,
/// Handle a fan request under '/fan/<id>/</turn_on/turn_off/toggle>'. /// Handle a fan request under '/fan/<id>/</turn_on/turn_off/toggle>'.
void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string fan_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> fan_state_json_generator(WebServer *web_server, void *source);
static std::string fan_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> fan_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
@@ -333,8 +334,8 @@ class WebServer : public Controller,
/// Handle a light request under '/light/<id>/</turn_on/turn_off/toggle>'. /// Handle a light request under '/light/<id>/</turn_on/turn_off/toggle>'.
void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string light_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> light_state_json_generator(WebServer *web_server, void *source);
static std::string light_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> light_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
@@ -343,8 +344,8 @@ class WebServer : public Controller,
/// Handle a text sensor request under '/text_sensor/<id>'. /// Handle a text sensor request under '/text_sensor/<id>'.
void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string text_sensor_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> text_sensor_state_json_generator(WebServer *web_server, void *source);
static std::string text_sensor_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> text_sensor_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
@@ -353,8 +354,8 @@ class WebServer : public Controller,
/// Handle a cover request under '/cover/<id>/<open/close/stop/set>'. /// Handle a cover request under '/cover/<id>/<open/close/stop/set>'.
void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string cover_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> cover_state_json_generator(WebServer *web_server, void *source);
static std::string cover_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> cover_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
@@ -362,8 +363,8 @@ class WebServer : public Controller,
/// Handle a number request under '/number/<id>'. /// Handle a number request under '/number/<id>'.
void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string number_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> number_state_json_generator(WebServer *web_server, void *source);
static std::string number_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> number_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
@@ -371,8 +372,8 @@ class WebServer : public Controller,
/// Handle a date request under '/date/<id>'. /// Handle a date request under '/date/<id>'.
void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string date_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> date_state_json_generator(WebServer *web_server, void *source);
static std::string date_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> date_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
@@ -380,8 +381,8 @@ class WebServer : public Controller,
/// Handle a time request under '/time/<id>'. /// Handle a time request under '/time/<id>'.
void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string time_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> time_state_json_generator(WebServer *web_server, void *source);
static std::string time_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> time_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
@@ -389,8 +390,8 @@ class WebServer : public Controller,
/// Handle a datetime request under '/datetime/<id>'. /// Handle a datetime request under '/datetime/<id>'.
void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string datetime_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> datetime_state_json_generator(WebServer *web_server, void *source);
static std::string datetime_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> datetime_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
@@ -398,8 +399,8 @@ class WebServer : public Controller,
/// Handle a text input request under '/text/<id>'. /// Handle a text input request under '/text/<id>'.
void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string text_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> text_state_json_generator(WebServer *web_server, void *source);
static std::string text_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> text_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
@@ -407,8 +408,8 @@ class WebServer : public Controller,
/// Handle a select request under '/select/<id>'. /// Handle a select request under '/select/<id>'.
void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string select_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> select_state_json_generator(WebServer *web_server, void *source);
static std::string select_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> select_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
@@ -416,8 +417,8 @@ class WebServer : public Controller,
/// Handle a climate request under '/climate/<id>'. /// Handle a climate request under '/climate/<id>'.
void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string climate_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> climate_state_json_generator(WebServer *web_server, void *source);
static std::string climate_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> climate_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
@@ -426,8 +427,8 @@ class WebServer : public Controller,
/// Handle a lock request under '/lock/<id>/</lock/unlock/open>'. /// Handle a lock request under '/lock/<id>/</lock/unlock/open>'.
void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string lock_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> lock_state_json_generator(WebServer *web_server, void *source);
static std::string lock_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> lock_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
@@ -436,8 +437,8 @@ class WebServer : public Controller,
/// Handle a valve request under '/valve/<id>/<open/close/stop/set>'. /// Handle a valve request under '/valve/<id>/<open/close/stop/set>'.
void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string valve_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> valve_state_json_generator(WebServer *web_server, void *source);
static std::string valve_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> valve_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
@@ -446,8 +447,8 @@ class WebServer : public Controller,
/// Handle a alarm_control_panel request under '/alarm_control_panel/<id>'. /// Handle a alarm_control_panel request under '/alarm_control_panel/<id>'.
void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string alarm_control_panel_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> alarm_control_panel_state_json_generator(WebServer *web_server, void *source);
static std::string alarm_control_panel_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> alarm_control_panel_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_WATER_HEATER #ifdef USE_WATER_HEATER
@@ -456,22 +457,22 @@ class WebServer : public Controller,
/// Handle a water_heater request under '/water_heater/<id>/<mode/set>'. /// Handle a water_heater request under '/water_heater/<id>/<mode/set>'.
void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string water_heater_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> water_heater_state_json_generator(WebServer *web_server, void *source);
static std::string water_heater_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> water_heater_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_INFRARED #ifdef USE_INFRARED
/// Handle an infrared request under '/infrared/<id>/transmit'. /// Handle an infrared request under '/infrared/<id>/transmit'.
void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string infrared_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> infrared_all_json_generator(WebServer *web_server, void *source);
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
void on_event(event::Event *obj) override; void on_event(event::Event *obj) override;
static std::string event_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> event_state_json_generator(WebServer *web_server, void *source);
static std::string event_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> event_all_json_generator(WebServer *web_server, void *source);
/// Handle a event request under '/event<id>'. /// Handle a event request under '/event<id>'.
void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -483,8 +484,8 @@ class WebServer : public Controller,
/// Handle a update request under '/update/<id>'. /// Handle a update request under '/update/<id>'.
void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match); void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string update_state_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> update_state_json_generator(WebServer *web_server, void *source);
static std::string update_all_json_generator(WebServer *web_server, void *source); static json::SerializationBuffer<> update_all_json_generator(WebServer *web_server, void *source);
#endif #endif
/// Override the web handler's canHandle method. /// Override the web handler's canHandle method.
@@ -609,71 +610,74 @@ class WebServer : public Controller,
private: private:
#ifdef USE_SENSOR #ifdef USE_SENSOR
std::string sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config); json::SerializationBuffer<> sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config);
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
std::string switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config); json::SerializationBuffer<> switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config);
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
std::string button_json_(button::Button *obj, JsonDetail start_config); json::SerializationBuffer<> button_json_(button::Button *obj, JsonDetail start_config);
#endif #endif
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
std::string binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config); json::SerializationBuffer<> binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value,
JsonDetail start_config);
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
std::string fan_json_(fan::Fan *obj, JsonDetail start_config); json::SerializationBuffer<> fan_json_(fan::Fan *obj, JsonDetail start_config);
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
std::string light_json_(light::LightState *obj, JsonDetail start_config); json::SerializationBuffer<> light_json_(light::LightState *obj, JsonDetail start_config);
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
std::string text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config); json::SerializationBuffer<> text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value,
JsonDetail start_config);
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
std::string cover_json_(cover::Cover *obj, JsonDetail start_config); json::SerializationBuffer<> cover_json_(cover::Cover *obj, JsonDetail start_config);
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
std::string number_json_(number::Number *obj, float value, JsonDetail start_config); json::SerializationBuffer<> number_json_(number::Number *obj, float value, JsonDetail start_config);
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
std::string date_json_(datetime::DateEntity *obj, JsonDetail start_config); json::SerializationBuffer<> date_json_(datetime::DateEntity *obj, JsonDetail start_config);
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
std::string time_json_(datetime::TimeEntity *obj, JsonDetail start_config); json::SerializationBuffer<> time_json_(datetime::TimeEntity *obj, JsonDetail start_config);
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
std::string datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config); json::SerializationBuffer<> datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config);
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
std::string text_json_(text::Text *obj, const std::string &value, JsonDetail start_config); json::SerializationBuffer<> text_json_(text::Text *obj, const std::string &value, JsonDetail start_config);
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
std::string select_json_(select::Select *obj, StringRef value, JsonDetail start_config); json::SerializationBuffer<> select_json_(select::Select *obj, StringRef value, JsonDetail start_config);
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
std::string climate_json_(climate::Climate *obj, JsonDetail start_config); json::SerializationBuffer<> climate_json_(climate::Climate *obj, JsonDetail start_config);
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
std::string lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config); json::SerializationBuffer<> lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config);
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
std::string valve_json_(valve::Valve *obj, JsonDetail start_config); json::SerializationBuffer<> valve_json_(valve::Valve *obj, JsonDetail start_config);
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
std::string alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, json::SerializationBuffer<> alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config); alarm_control_panel::AlarmControlPanelState value,
JsonDetail start_config);
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
std::string event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config); json::SerializationBuffer<> event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config);
#endif #endif
#ifdef USE_WATER_HEATER #ifdef USE_WATER_HEATER
std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config); json::SerializationBuffer<> water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config);
#endif #endif
#ifdef USE_INFRARED #ifdef USE_INFRARED
std::string infrared_json_(infrared::Infrared *obj, JsonDetail start_config); json::SerializationBuffer<> infrared_json_(infrared::Infrared *obj, JsonDetail start_config);
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config); json::SerializationBuffer<> update_json_(update::UpdateEntity *obj, JsonDetail start_config);
#endif #endif
}; };

View File

@@ -53,4 +53,4 @@ async def to_code(config):
"lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"] "lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"]
) )
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.6") cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.5")

View File

@@ -507,7 +507,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
// Configure reconnect timeout and send config // Configure reconnect timeout and send config
// this should always go through since the tcp send buffer is empty on connect // this should always go through since the tcp send buffer is empty on connect
std::string message = ws->get_config_json(); auto message = ws->get_config_json();
this->try_send_nodefer(message.c_str(), "ping", millis(), 30000); this->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
#ifdef USE_WEBSERVER_SORTING #ifdef USE_WEBSERVER_SORTING
@@ -561,7 +561,7 @@ void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_g
void AsyncEventSourceResponse::process_deferred_queue_() { void AsyncEventSourceResponse::process_deferred_queue_() {
while (!deferred_queue_.empty()) { while (!deferred_queue_.empty()) {
DeferredEvent &de = deferred_queue_.front(); DeferredEvent &de = deferred_queue_.front();
std::string message = de.message_generator_(web_server_, de.source_); auto message = de.message_generator_(web_server_, de.source_);
if (this->try_send_nodefer(message.c_str(), "state")) { if (this->try_send_nodefer(message.c_str(), "state")) {
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
deferred_queue_.erase(deferred_queue_.begin()); deferred_queue_.erase(deferred_queue_.begin());
@@ -798,7 +798,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e
// trying to send first // trying to send first
deq_push_back_with_dedup_(source, message_generator); deq_push_back_with_dedup_(source, message_generator);
} else { } else {
std::string message = message_generator(web_server_, source); auto message = message_generator(web_server_, source);
if (!this->try_send_nodefer(message.c_str(), "state")) { if (!this->try_send_nodefer(message.c_str(), "state")) {
deq_push_back_with_dedup_(source, message_generator); deq_push_back_with_dedup_(source, message_generator);
} }

View File

@@ -16,6 +16,7 @@
#include <vector> #include <vector>
#ifdef USE_WEBSERVER #ifdef USE_WEBSERVER
#include "esphome/components/json/json_util.h"
#include "esphome/components/web_server/list_entities.h" #include "esphome/components/web_server/list_entities.h"
#endif #endif
@@ -250,7 +251,7 @@ class AsyncWebHandler {
class AsyncEventSource; class AsyncEventSource;
class AsyncEventSourceResponse; class AsyncEventSourceResponse;
using message_generator_t = std::string(esphome::web_server::WebServer *, void *); using message_generator_t = json::SerializationBuffer<>(esphome::web_server::WebServer *, void *);
/* /*
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function

View File

@@ -278,13 +278,9 @@ LAMBDA_PROG = re.compile(r"\bid\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)")
class Lambda: class Lambda:
def __init__(self, value): def __init__(self, value):
from esphome.cpp_generator import Expression, statement
# pylint: disable=protected-access # pylint: disable=protected-access
if isinstance(value, Lambda): if isinstance(value, Lambda):
self._value = value._value self._value = value._value
elif isinstance(value, Expression):
self._value = str(statement(value))
else: else:
self._value = value self._value = value
self._parts = None self._parts = None

View File

@@ -210,7 +210,7 @@ void Application::loop() {
#ifdef USE_ESP32 #ifdef USE_ESP32
esp_chip_info_t chip_info; esp_chip_info_t chip_info;
esp_chip_info(&chip_info); esp_chip_info(&chip_info);
ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, ESP_LOGI(TAG, "ESP32 Chip: %s r%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100,
chip_info.revision % 100, chip_info.cores); chip_info.revision % 100, chip_info.cores);
#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) #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 // Suggest optimization for chips that don't need the PSRAM cache workaround

View File

@@ -1,61 +0,0 @@
#pragma once
#ifdef USE_ESP32
#include <freertos/FreeRTOS.h>
namespace esphome {
/// @brief FreeRTOS task priority definitions for ESPHome
///
/// All priorities use relative values based on configMAX_PRIORITIES so they
/// scale automatically if CONFIG_FREERTOS_MAX_PRIORITIES changes.
///
/// Priority hierarchy (with CONFIG_FREERTOS_MAX_PRIORITIES = 16):
///
/// 14: Audio capture (I2S microphone) - highest, real-time audio input
/// 10: Audio output (I2S speaker) - real-time audio output
/// 8: Network stack (LWIP TCP/IP) - set via CONFIG_LWIP_TCPIP_TASK_PRIO
/// 6: Audio mixing - buffered audio processing
/// 5: Protocol tasks (MQTT, USB host, OpenThread) - communication
/// 4: USB serial TX - serial communication
/// 3: ML inference (wake word) - background ML processing
/// 1: Application (main loop, media pipelines, camera, UART RX) - baseline
/// 0: Idle task (FreeRTOS system)
///
/// Guidelines:
/// - Real-time audio I/O tasks need highest priorities to prevent glitches
/// - Network/protocol tasks should be above application but below audio
/// - Background processing (ML, media decoding) can run at low priority
/// Audio capture task priority (I2S microphone)
/// Highest application priority - audio input cannot tolerate delays
static constexpr UBaseType_t TASK_PRIORITY_AUDIO_CAPTURE = configMAX_PRIORITIES - 2;
/// Audio output task priority (I2S speaker)
/// High priority - audio output needs consistent timing
static constexpr UBaseType_t TASK_PRIORITY_AUDIO_OUTPUT = configMAX_PRIORITIES - 6;
/// Audio mixer task priority
/// Medium-high - mixing is buffered but feeds real-time output
static constexpr UBaseType_t TASK_PRIORITY_AUDIO_MIXER = configMAX_PRIORITIES - 10;
/// Protocol/communication task priority (MQTT, USB host, OpenThread)
/// Above application tasks for responsive network handling
static constexpr UBaseType_t TASK_PRIORITY_PROTOCOL = configMAX_PRIORITIES - 11;
/// USB serial TX task priority
/// Slightly below protocol tasks
static constexpr UBaseType_t TASK_PRIORITY_USB_SERIAL = configMAX_PRIORITIES - 12;
/// ML inference task priority (wake word detection)
/// Background processing - can yield to communication tasks
static constexpr UBaseType_t TASK_PRIORITY_INFERENCE = configMAX_PRIORITIES - 13;
/// Application task priority (main loop, media pipelines, camera, etc.)
/// Baseline priority - just above idle task
static constexpr UBaseType_t TASK_PRIORITY_APPLICATION = tskIDLE_PRIORITY + 1;
} // namespace esphome
#endif // USE_ESP32

View File

@@ -462,16 +462,6 @@ def statement(expression: Expression | Statement) -> Statement:
return ExpressionStatement(expression) 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( def variable(
id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True
) -> "MockObj": ) -> "MockObj":
@@ -675,7 +665,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
async def process_lambda( async def process_lambda(
value: Lambda | Expression, value: Lambda,
parameters: TemplateArgsType, parameters: TemplateArgsType,
capture: str = "", capture: str = "",
return_type: SafeExpType = None, return_type: SafeExpType = None,
@@ -699,14 +689,6 @@ async def process_lambda(
if value is None: if value is None:
return 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[:] parts = value.parts[:]
for i, id in enumerate(value.requires_ids): for i, id in enumerate(value.requires_ids):
full_id, var = await get_variable_with_full_id(id) full_id, var = await get_variable_with_full_id(id)

View File

@@ -114,7 +114,7 @@ lib_deps =
ESP8266WiFi ; wifi (Arduino built-in) ESP8266WiFi ; wifi (Arduino built-in)
Update ; ota (Arduino built-in) Update ; ota (Arduino built-in)
ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base
makuna/NeoPixelBus@2.7.3 ; neopixelbus makuna/NeoPixelBus@2.7.3 ; neopixelbus
ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266HTTPClient ; http_request (Arduino built-in)
ESP8266mDNS ; mdns (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in)
@@ -202,7 +202,7 @@ lib_deps =
${common:arduino.lib_deps} ${common:arduino.lib_deps}
ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp
bblanchon/ArduinoJson@7.4.2 ; json bblanchon/ArduinoJson@7.4.2 ; json
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base
build_flags = build_flags =
${common:arduino.build_flags} ${common:arduino.build_flags}
-DUSE_RP2040 -DUSE_RP2040
@@ -218,7 +218,7 @@ framework = arduino
lib_compat_mode = soft lib_compat_mode = soft
lib_deps = lib_deps =
bblanchon/ArduinoJson@7.4.2 ; json bblanchon/ArduinoJson@7.4.2 ; json
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base
droscy/esp_wireguard@0.4.2 ; wireguard droscy/esp_wireguard@0.4.2 ; wireguard
build_flags = build_flags =
${common:arduino.build_flags} ${common:arduino.build_flags}

View File

@@ -1,11 +0,0 @@
dlms_meter:
decryption_key: "36C66639E48A8CA4D6BC8B282A793BBB" # change this to your decryption key!
sensor:
- platform: dlms_meter
reactive_energy_plus:
name: "Reactive energy taken from grid"
reactive_energy_minus:
name: "Reactive energy put into grid"
<<: !include common.yaml

View File

@@ -1,17 +0,0 @@
dlms_meter:
decryption_key: "36C66639E48A8CA4D6BC8B282A793BBB" # change this to your decryption key!
provider: netznoe # (optional) key - only set if using evn
sensor:
- platform: dlms_meter
# EVN
power_factor:
name: "Power Factor"
text_sensor:
- platform: dlms_meter
# EVN
meternumber:
name: "meterNumber"
<<: !include common.yaml

View File

@@ -1,27 +0,0 @@
sensor:
- platform: dlms_meter
voltage_l1:
name: "Voltage L1"
voltage_l2:
name: "Voltage L2"
voltage_l3:
name: "Voltage L3"
current_l1:
name: "Current L1"
current_l2:
name: "Current L2"
current_l3:
name: "Current L3"
active_power_plus:
name: "Active power taken from grid"
active_power_minus:
name: "Active power put into grid"
active_energy_plus:
name: "Active energy taken from grid"
active_energy_minus:
name: "Active energy put into grid"
text_sensor:
- platform: dlms_meter
timestamp:
name: "timestamp"

View File

@@ -1,4 +0,0 @@
packages:
uart: !include ../../test_build_components/common/uart_2400/esp32-ard.yaml
<<: !include common-generic.yaml

View File

@@ -1,4 +0,0 @@
packages:
uart: !include ../../test_build_components/common/uart_2400/esp32-idf.yaml
<<: !include common-netznoe.yaml

View File

@@ -1,4 +0,0 @@
packages:
uart: !include ../../test_build_components/common/uart_2400/esp8266-ard.yaml
<<: !include common-generic.yaml

View File

@@ -4,15 +4,16 @@ interval:
- interval: 60s - interval: 60s
then: then:
- lambda: |- - lambda: |-
// Test build_json // Test build_json - returns SerializationBuffer, use auto to avoid heap allocation
std::string json_str = esphome::json::build_json([](JsonObject root) { auto json_buf = esphome::json::build_json([](JsonObject root) {
root["sensor"] = "temperature"; root["sensor"] = "temperature";
root["value"] = 23.5; root["value"] = 23.5;
root["unit"] = "°C"; root["unit"] = "°C";
}); });
ESP_LOGD("test", "Built JSON: %s", json_str.c_str()); ESP_LOGD("test", "Built JSON: %s", json_buf.c_str());
// Test parse_json // Test parse_json - implicit conversion to std::string for backward compatibility
std::string json_str = json_buf;
bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) { bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) {
if (root["sensor"].is<const char*>() && root["value"].is<float>()) { if (root["sensor"].is<const char*>() && root["value"].is<float>()) {
const char* sensor = root["sensor"]; const char* sensor = root["sensor"];
@@ -26,10 +27,10 @@ interval:
}); });
ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed"); ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed");
// Test JsonBuilder class // Test JsonBuilder class - returns SerializationBuffer
esphome::json::JsonBuilder builder; esphome::json::JsonBuilder builder;
JsonObject obj = builder.root(); JsonObject obj = builder.root();
obj["test"] = "direct_builder"; obj["test"] = "direct_builder";
obj["count"] = 42; obj["count"] = 42;
std::string result = builder.serialize(); auto result = builder.serialize();
ESP_LOGD("test", "JsonBuilder result: %s", result.c_str()); ESP_LOGD("test", "JsonBuilder result: %s", result.c_str());

View File

@@ -1,10 +0,0 @@
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: pm_10_0:
name: PM 10.0 Concentration name: PM 10.0 Concentration
pm_1_0_std: pm_1_0_std:
name: PM 1.0 Standard Atmospheric Concentration name: PM 1.0 Standard Atmospher Concentration
pm_2_5_std: pm_2_5_std:
name: PM 2.5 Standard Atmospheric Concentration name: PM 2.5 Standard Atmospher Concentration
pm_10_0_std: pm_10_0_std:
name: PM 10.0 Standard Atmospheric Concentration name: PM 10.0 Standard Atmospher Concentration
pm_0_3um: pm_0_3um:
name: Particulate Count >0.3um name: Particulate Count >0.3um
pm_0_5um: pm_0_5um:

View File

@@ -1,11 +0,0 @@
# Common UART configuration for ESP32 Arduino tests - 2400 baud
substitutions:
tx_pin: GPIO17
rx_pin: GPIO16
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 2400

View File

@@ -1,11 +0,0 @@
# Common UART configuration for ESP32 IDF tests - 2400 baud
substitutions:
tx_pin: GPIO17
rx_pin: GPIO16
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 2400

View File

@@ -1,11 +0,0 @@
# Common UART configuration for ESP8266 Arduino tests - 2400 baud
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 2400

View File

@@ -347,280 +347,3 @@ class TestMockObj:
assert isinstance(actual, cg.MockObj) assert isinstance(actual, cg.MockObj)
assert actual.base == "foo.eek" assert actual.base == "foo.eek"
assert actual.op == "." 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,)])