Compare commits

..

7 Commits

Author SHA1 Message Date
J. Nick Koston
0b444f5504 [esp32] Centralize FreeRTOS task priorities and reduce MAX_PRIORITIES to 16 2026-01-31 11:58:30 -06:00
J. Nick Koston
6f6c252b84 [esp32] Centralize FreeRTOS task priorities and reduce MAX_PRIORITIES to 16 2026-01-31 11:54:14 -06:00
J. Nick Koston
0b325b5a0a [esp32] Centralize FreeRTOS task priorities and reduce MAX_PRIORITIES to 16 2026-01-31 11:51:33 -06:00
Simon Fischer
1ff2f3b6a3 [dlms_meter] Add dlms smart meter component (#8009)
Co-authored-by: Thomas Rupprecht <rupprecht.thomas@gmail.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 10:48:27 -05:00
Jonathan Swoboda
891382a32e [max7219] Allocate buffer in constructor (#13660)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 09:59:13 -05:00
J. Nick Koston
0fd50b2381 [esp32] Disable unused per-tag log filtering, saving ~536 bytes RAM (#13662) 2026-01-31 01:21:52 -06:00
Clyde Stubbs
9dcb469460 [core] Simplify generation of Lambda during to_code() (#13533) 2026-01-31 12:18:30 +11:00
46 changed files with 1597 additions and 98 deletions

View File

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

View File

@@ -0,0 +1,57 @@
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

@@ -0,0 +1,71 @@
#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

@@ -0,0 +1,468 @@
#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

@@ -0,0 +1,96 @@
#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

@@ -0,0 +1,69 @@
#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

@@ -0,0 +1,94 @@
#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

@@ -0,0 +1,124 @@
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

@@ -0,0 +1,37 @@
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

@@ -1287,6 +1287,18 @@ async def to_code(config):
# Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms
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
# 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
@@ -1329,6 +1341,10 @@ async def to_code(config):
# Disable dynamic log level control to save memory
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
# Disable per-tag log level filtering since dynamic level control is disabled above
# This saves ~250 bytes of RAM (tag cache) and associated code
add_idf_sdkconfig_option("CONFIG_LOG_TAG_LEVEL_IMPL_NONE", True)
# Reduce PHY TX power in the event of a brownout
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)

View File

@@ -3,6 +3,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/task_priorities.h"
#include "preferences.h"
#include <esp_clk_tree.h>
#include <esp_cpu.h>
@@ -66,10 +67,14 @@ void loop_task(void *pv_params) {
extern "C" void app_main() {
initArduino();
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
xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle);
xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, TASK_PRIORITY_APPLICATION,
&loop_task_handle);
#else
xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1);
xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, TASK_PRIORITY_APPLICATION,
&loop_task_handle, 1);
#endif
}

View File

@@ -4,6 +4,7 @@
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include <freertos/task.h>
@@ -42,11 +43,13 @@ void ESP32Camera::setup() {
/* initialize RTOS */
this->framebuffer_get_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,
"framebuffer_task", // name
FRAMEBUFFER_TASK_STACK_SIZE, // stack size
this, // task pv params
1, // priority
TASK_PRIORITY_APPLICATION, // priority
nullptr, // handle
1 // core
);

View File

@@ -34,29 +34,14 @@ static const char *const ESP_HOSTED_VERSION_STR = STRINGIFY(ESP_HOSTED_VERSION_M
ESP_HOSTED_VERSION_MINOR_1) "." STRINGIFY(ESP_HOSTED_VERSION_PATCH_1);
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// Parse an integer from str, advancing ptr past the number
// Returns false if no digits were parsed
static bool parse_int(const char *&ptr, int &value) {
char *end;
value = static_cast<int>(strtol(ptr, &end, 10));
if (end == ptr)
return false;
ptr = end;
return true;
}
// Parse version string "major.minor.patch" into components
// Returns true if at least major.minor was parsed
// Returns true if parsing succeeded
static bool parse_version(const std::string &version_str, int &major, int &minor, int &patch) {
major = minor = patch = 0;
const char *ptr = version_str.c_str();
if (!parse_int(ptr, major) || *ptr++ != '.' || !parse_int(ptr, minor))
return false;
if (*ptr == '.')
parse_int(++ptr, patch);
return true;
if (sscanf(version_str.c_str(), "%d.%d.%d", &major, &minor, &patch) >= 2) {
return true;
}
return false;
}
// Compare two versions, returns:

View File

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

View File

@@ -11,12 +11,6 @@ namespace i2c {
static const char *const TAG = "i2c";
void I2CBus::i2c_scan_() {
// suppress logs from the IDF I2C library during the scan
#if defined(USE_ESP32) && defined(USE_LOGGER)
auto previous = esp_log_level_get("*");
esp_log_level_set("*", ESP_LOG_NONE);
#endif
for (uint8_t address = 8; address != 120; address++) {
auto err = write_readv(address, nullptr, 0, nullptr, 0);
if (err == ERROR_OK) {
@@ -27,9 +21,6 @@ void I2CBus::i2c_scan_() {
// it takes 16sec to scan on nrf52. It prevents board reset.
arch_feed_wdt();
}
#if defined(USE_ESP32) && defined(USE_LOGGER)
esp_log_level_set("*", previous);
#endif
}
ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len) {

View File

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

View File

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

View File

@@ -114,9 +114,6 @@ void Logger::pre_setup() {
global_logger = this;
esp_log_set_vprintf(esp_idf_log_vprintf_);
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
esp_log_level_set("*", ESP_LOG_VERBOSE);
}
ESP_LOGI(TAG, "Log initialized");
}

View File

@@ -28,11 +28,10 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
var = cg.new_Pvariable(config[CONF_ID], config[CONF_NUM_CHIPS])
await spi.register_spi_device(var, config, write_only=True)
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_reverse(config[CONF_REVERSE_ENABLE]))

View File

@@ -3,8 +3,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace max7219 {
namespace esphome::max7219 {
static const char *const TAG = "max7219";
@@ -115,12 +114,14 @@ const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = {
};
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() {
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
this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7);
// let's use our own ASCII -> led pattern encoding
@@ -229,7 +230,6 @@ void MAX7219Component::set_intensity(uint8_t 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) {
char buffer[64];
@@ -240,5 +240,4 @@ 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); }
} // namespace max7219
} // namespace esphome
} // namespace esphome::max7219

View File

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

View File

@@ -6,6 +6,7 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include "esphome/components/audio/audio_transfer_buffer.h"
@@ -25,7 +26,6 @@ static const size_t DATA_TIMEOUT_MS = 50;
static const uint32_t RING_BUFFER_DURATION_MS = 120;
static const uint32_t INFERENCE_TASK_STACK_SIZE = 3072;
static const UBaseType_t INFERENCE_TASK_PRIORITY = 3;
enum EventGroupBits : uint32_t {
COMMAND_STOP = (1 << 0), // Signals the inference task should stop
@@ -305,8 +305,10 @@ void MicroWakeWord::loop() {
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,
INFERENCE_TASK_PRIORITY, &this->inference_task_handle_);
TASK_PRIORITY_INFERENCE, &this->inference_task_handle_);
if (this->inference_task_handle_ == nullptr) {
FrontendFreeStateContents(&this->frontend_state_); // Deallocate frontend state

View File

@@ -5,6 +5,7 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include <algorithm>
#include <cstring>
@@ -12,8 +13,6 @@
namespace esphome {
namespace mixer_speaker {
static const UBaseType_t MIXER_TASK_PRIORITY = 10;
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
static const uint32_t TASK_DELAY_MS = 25;
@@ -385,8 +384,10 @@ esp_err_t MixerSpeaker::start_task_() {
}
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,
MIXER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_);
TASK_PRIORITY_AUDIO_MIXER, this->task_stack_buffer_, &this->task_stack_);
}
if (this->task_handle_ == nullptr) {

View File

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

View File

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

View File

@@ -10,6 +10,7 @@
#include "esp_task_wdt.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include "esp_err.h"
#include "esp_event.h"
@@ -39,12 +40,14 @@ void OpenThreadComponent::setup() {
ESP_ERROR_CHECK(esp_netif_init());
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(
[](void *arg) {
static_cast<OpenThreadComponent *>(arg)->ot_main();
vTaskDelete(nullptr);
},
"ot_main", 10240, this, 5, nullptr);
"ot_main", 10240, this, TASK_PRIORITY_PROTOCOL, nullptr);
}
static esp_netif_t *init_openthread_netif(const esp_openthread_platform_config_t *config) {

View File

@@ -6,6 +6,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/task_priorities.h"
#include <algorithm>
#include <cstring>
@@ -13,8 +14,6 @@
namespace esphome {
namespace resampler {
static const UBaseType_t RESAMPLER_TASK_PRIORITY = 1;
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
static const uint32_t TASK_DELAY_MS = 20;
@@ -185,8 +184,10 @@ esp_err_t ResamplerSpeaker::start_task_() {
}
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,
RESAMPLER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_);
TASK_PRIORITY_APPLICATION, this->task_stack_buffer_, &this->task_stack_);
}
if (this->task_handle_ == nullptr) {

View File

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

View File

@@ -7,6 +7,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/gpio.h"
#include "esphome/core/task_priorities.h"
#include "driver/gpio.h"
#include "soc/gpio_num.h"
#include "soc/uart_pins.h"
@@ -367,12 +368,13 @@ void IDFUARTComponent::check_logger_conflict() {}
#ifdef USE_UART_WAKE_LOOP_ON_RX
void IDFUARTComponent::start_rx_event_task_() {
// Create FreeRTOS task to monitor UART events
BaseType_t result = xTaskCreate(rx_event_task_func, // Task function
"uart_rx_evt", // Task name (max 16 chars)
2240, // Stack size in bytes (~2.2KB); increase if needed for logging
this, // Task parameter (this pointer)
tskIDLE_PRIORITY + 1, // Priority (low, just above idle)
// TASK_PRIORITY_APPLICATION: same as main loop - UART RX monitoring is lightweight,
// just wakes main loop when data arrives
BaseType_t result = xTaskCreate(rx_event_task_func, // Task function
"uart_rx_evt", // Task name (max 16 chars)
2240, // Stack size in bytes (~2.2KB)
this, // Task parameter (this pointer)
TASK_PRIORITY_APPLICATION,
&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
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
from esphome.core import ID, Lambda
from esphome.cpp_generator import ExpressionStatement, MockObj
from esphome.core import ID
from esphome.cpp_generator import literal
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["network"]
@@ -24,6 +24,8 @@ udp_ns = cg.esphome_ns.namespace("udp")
UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
trigger_args = cg.std_vector.template(cg.uint8)
trigger_argname = "data"
trigger_argtype = [(trigger_args, trigger_argname)]
CONF_ADDRESSES = "addresses"
CONF_LISTEN_ADDRESS = "listen_address"
@@ -111,13 +113,14 @@ async def to_code(config):
cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]]))
if on_receive := config.get(CONF_ON_RECEIVE):
on_receive = on_receive[0]
trigger = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID])
trigger_id = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID])
trigger = await automation.build_automation(
trigger, [(trigger_args, "data")], on_receive
trigger_id, trigger_argtype, on_receive
)
trigger = Lambda(str(ExpressionStatement(trigger.trigger(MockObj("data")))))
trigger = await cg.process_lambda(trigger, [(trigger_args, "data")])
cg.add(var.add_listener(trigger))
trigger_lambda = await cg.process_lambda(
trigger.trigger(literal(trigger_argname)), trigger_argtype
)
cg.add(var.add_listener(trigger_lambda))
cg.add(var.set_should_listen())

View File

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

View File

@@ -4,6 +4,7 @@
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#include "esphome/core/task_priorities.h"
#include <vector>
#include "usb/usb_host.h"
#include <freertos/FreeRTOS.h>
@@ -69,7 +70,6 @@ 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_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
struct TransferStatus {

View File

@@ -215,11 +215,12 @@ void USBClient::setup() {
}
// 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",
USB_TASK_STACK_SIZE, // Stack size
this, // Task parameter
USB_TASK_PRIORITY, // Priority (higher than main loop)
&this->usb_task_handle_);
TASK_PRIORITY_PROTOCOL, &this->usb_task_handle_);
if (this->usb_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Failed to create USB task");

View File

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

View File

@@ -0,0 +1,61 @@
#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,6 +462,16 @@ def statement(expression: Expression | Statement) -> Statement:
return ExpressionStatement(expression)
def literal(name: str) -> "MockObj":
"""Create a literal name that will appear in the generated code
not surrounded by quotes.
:param name: The name of the literal.
:return: The literal as a MockObj.
"""
return MockObj(name, "")
def variable(
id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True
) -> "MockObj":
@@ -665,7 +675,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
async def process_lambda(
value: Lambda,
value: Lambda | Expression,
parameters: TemplateArgsType,
capture: str = "",
return_type: SafeExpType = None,
@@ -689,6 +699,14 @@ async def process_lambda(
if value is None:
return None
# Inadvertently passing a malformed parameters value will lead to the build process mysteriously hanging at the
# "Generating C++ source..." stage, so check here to save the developer's hair.
assert isinstance(parameters, list) and all(
isinstance(p, tuple) and len(p) == 2 for p in parameters
)
if isinstance(value, Expression):
value = Lambda(value)
parts = value.parts[:]
for i, id in enumerate(value.requires_ids):
full_id, var = await get_variable_with_full_id(id)

View File

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,17 @@
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

@@ -0,0 +1,27 @@
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

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
# 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

@@ -0,0 +1,11 @@
# 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

@@ -0,0 +1,11 @@
# 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,3 +347,280 @@ class TestMockObj:
assert isinstance(actual, cg.MockObj)
assert actual.base == "foo.eek"
assert actual.op == "."
class TestStatementFunction:
"""Tests for the statement() function."""
def test_statement__expression_converted_to_statement(self):
"""Test that expressions are converted to ExpressionStatement."""
expr = cg.RawExpression("foo()")
result = cg.statement(expr)
assert isinstance(result, cg.ExpressionStatement)
assert str(result) == "foo();"
def test_statement__statement_unchanged(self):
"""Test that statements are returned unchanged."""
stmt = cg.RawStatement("foo()")
result = cg.statement(stmt)
assert result is stmt
assert str(result) == "foo()"
def test_statement__expression_statement_unchanged(self):
"""Test that ExpressionStatement is returned unchanged."""
stmt = cg.ExpressionStatement(42)
result = cg.statement(stmt)
assert result is stmt
assert str(result) == "42;"
def test_statement__line_comment_unchanged(self):
"""Test that LineComment is returned unchanged."""
stmt = cg.LineComment("This is a comment")
result = cg.statement(stmt)
assert result is stmt
assert str(result) == "// This is a comment"
class TestLiteralFunction:
"""Tests for the literal() function."""
def test_literal__creates_mockobj(self):
"""Test that literal() creates a MockObj."""
result = cg.literal("MY_CONSTANT")
assert isinstance(result, cg.MockObj)
assert result.base == "MY_CONSTANT"
assert result.op == ""
def test_literal__string_representation(self):
"""Test that literal names appear unquoted in generated code."""
result = cg.literal("nullptr")
assert str(result) == "nullptr"
def test_literal__can_be_used_in_expressions(self):
"""Test that literals can be used as part of larger expressions."""
null_lit = cg.literal("nullptr")
expr = cg.CallExpression(cg.RawExpression("my_func"), null_lit)
assert str(expr) == "my_func(nullptr)"
def test_literal__common_cpp_literals(self):
"""Test common C++ literal values."""
test_cases = [
("nullptr", "nullptr"),
("true", "true"),
("false", "false"),
("NULL", "NULL"),
("NAN", "NAN"),
]
for name, expected in test_cases:
result = cg.literal(name)
assert str(result) == expected
class TestLambdaConstructor:
"""Tests for the Lambda class constructor in core/__init__.py."""
def test_lambda__from_string(self):
"""Test Lambda constructor with string argument."""
from esphome.core import Lambda
lambda_obj = Lambda("return x + 1;")
assert lambda_obj.value == "return x + 1;"
assert str(lambda_obj) == "return x + 1;"
def test_lambda__from_expression(self):
"""Test Lambda constructor with Expression argument."""
from esphome.core import Lambda
expr = cg.RawExpression("x + 1")
lambda_obj = Lambda(expr)
# Expression should be converted to statement (with semicolon)
assert lambda_obj.value == "x + 1;"
def test_lambda__from_lambda(self):
"""Test Lambda constructor with another Lambda argument."""
from esphome.core import Lambda
original = Lambda("return x + 1;")
copy = Lambda(original)
assert copy.value == original.value
assert copy.value == "return x + 1;"
def test_lambda__parts_parsing(self):
"""Test that Lambda correctly parses parts with id() references."""
from esphome.core import Lambda
lambda_obj = Lambda("return id(my_sensor).state;")
parts = lambda_obj.parts
# Parts should be split by LAMBDA_PROG regex: text, id, op, text
assert len(parts) == 4
assert parts[0] == "return "
assert parts[1] == "my_sensor"
assert parts[2] == "."
assert parts[3] == "state;"
def test_lambda__requires_ids(self):
"""Test that Lambda correctly extracts required IDs."""
from esphome.core import ID, Lambda
lambda_obj = Lambda("return id(sensor1).state + id(sensor2).value;")
ids = lambda_obj.requires_ids
assert len(ids) == 2
assert all(isinstance(id_obj, ID) for id_obj in ids)
assert ids[0].id == "sensor1"
assert ids[1].id == "sensor2"
def test_lambda__no_ids(self):
"""Test Lambda with no id() references."""
from esphome.core import Lambda
lambda_obj = Lambda("return 42;")
ids = lambda_obj.requires_ids
assert len(ids) == 0
def test_lambda__comment_removal(self):
"""Test that comments are removed when parsing parts."""
from esphome.core import Lambda
lambda_obj = Lambda("return id(sensor).state; // Get sensor state")
parts = lambda_obj.parts
# Comment should be replaced with space, not affect parsing
assert "my_sensor" not in str(parts)
def test_lambda__multiline_string(self):
"""Test Lambda with multiline string."""
from esphome.core import Lambda
code = """if (id(sensor).state > 0) {
return true;
}
return false;"""
lambda_obj = Lambda(code)
assert lambda_obj.value == code
assert "sensor" in [id_obj.id for id_obj in lambda_obj.requires_ids]
@pytest.mark.asyncio
class TestProcessLambda:
"""Tests for the process_lambda() async function."""
async def test_process_lambda__none_value(self):
"""Test that None returns None."""
result = await cg.process_lambda(None, [])
assert result is None
async def test_process_lambda__with_expression(self):
"""Test process_lambda with Expression argument."""
expr = cg.RawExpression("return x + 1")
result = await cg.process_lambda(expr, [(int, "x")])
assert isinstance(result, cg.LambdaExpression)
assert "x + 1" in str(result)
async def test_process_lambda__simple_lambda_no_ids(self):
"""Test process_lambda with simple Lambda without id() references."""
from esphome.core import Lambda
lambda_obj = Lambda("return x + 1;")
result = await cg.process_lambda(lambda_obj, [(int, "x")])
assert isinstance(result, cg.LambdaExpression)
# Should have parameter
lambda_str = str(result)
assert "int32_t x" in lambda_str
assert "return x + 1;" in lambda_str
async def test_process_lambda__with_return_type(self):
"""Test process_lambda with return type specified."""
from esphome.core import Lambda
lambda_obj = Lambda("return x > 0;")
result = await cg.process_lambda(lambda_obj, [(int, "x")], return_type=bool)
assert isinstance(result, cg.LambdaExpression)
lambda_str = str(result)
assert "-> bool" in lambda_str
async def test_process_lambda__with_capture(self):
"""Test process_lambda with capture specified."""
from esphome.core import Lambda
lambda_obj = Lambda("return captured + x;")
result = await cg.process_lambda(lambda_obj, [(int, "x")], capture="captured")
assert isinstance(result, cg.LambdaExpression)
lambda_str = str(result)
assert "[captured]" in lambda_str
async def test_process_lambda__empty_capture(self):
"""Test process_lambda with empty capture (stateless lambda)."""
from esphome.core import Lambda
lambda_obj = Lambda("return x + 1;")
result = await cg.process_lambda(lambda_obj, [(int, "x")], capture="")
assert isinstance(result, cg.LambdaExpression)
lambda_str = str(result)
assert "[]" in lambda_str
async def test_process_lambda__no_parameters(self):
"""Test process_lambda with no parameters."""
from esphome.core import Lambda
lambda_obj = Lambda("return 42;")
result = await cg.process_lambda(lambda_obj, [])
assert isinstance(result, cg.LambdaExpression)
lambda_str = str(result)
# Should have empty parameter list
assert "()" in lambda_str
async def test_process_lambda__multiple_parameters(self):
"""Test process_lambda with multiple parameters."""
from esphome.core import Lambda
lambda_obj = Lambda("return x + y + z;")
result = await cg.process_lambda(
lambda_obj, [(int, "x"), (float, "y"), (bool, "z")]
)
assert isinstance(result, cg.LambdaExpression)
lambda_str = str(result)
assert "int32_t x" in lambda_str
assert "float y" in lambda_str
assert "bool z" in lambda_str
async def test_process_lambda__parameter_validation(self):
"""Test that malformed parameters raise assertion error."""
from esphome.core import Lambda
lambda_obj = Lambda("return x;")
# Test invalid parameter format (not list of tuples)
with pytest.raises(AssertionError):
await cg.process_lambda(lambda_obj, "invalid")
# Test invalid tuple format (not 2-element tuples)
with pytest.raises(AssertionError):
await cg.process_lambda(lambda_obj, [(int, "x", "extra")])
# Test invalid tuple format (single element)
with pytest.raises(AssertionError):
await cg.process_lambda(lambda_obj, [(int,)])