Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy

This commit is contained in:
kbx81
2026-07-22 22:20:25 -05:00
4243 changed files with 162305 additions and 55795 deletions
+2
View File
@@ -0,0 +1,2 @@
/.esphome/
/secrets.yaml
@@ -0,0 +1,29 @@
import esphome.codegen as cg
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# api must run its to_code to define USE_API, USE_API_PLAINTEXT,
# and add the noise-c library dependency.
manifest.enable_codegen()
original_to_code = manifest.to_code
async def to_code(config):
await original_to_code(config)
# Enable proxy proto message types for benchmarks. The real
# components have hardware dependencies (BLE/UART/RMT); lightweight
# stub headers in tests/benchmarks/stubs/ satisfy the includes.
cg.add_define("USE_BLUETOOTH_PROXY")
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 3)
cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16)
cg.add_define("USE_ZWAVE_PROXY")
cg.add_define("USE_INFRARED")
cg.add_define("USE_IR_RF")
cg.add_define("USE_RADIO_FREQUENCY")
cg.add_define("USE_SERIAL_PROXY")
cg.add_define("SERIAL_PROXY_COUNT", 0)
cg.add_define("ESPHOME_ENTITY_INFRARED_COUNT", 0)
cg.add_define("ESPHOME_ENTITY_RADIO_FREQUENCY_COUNT", 0)
manifest.to_code = to_code
@@ -0,0 +1,67 @@
#pragma once
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <unistd.h>
#include <memory>
#include <utility>
#include "esphome/components/socket/socket.h"
namespace esphome::api::benchmarks {
// Helper to drain accumulated data from the read side of a socket
// to prevent the write side from blocking.
inline void drain_socket(int fd) {
char buf[65536];
while (::read(fd, buf, sizeof(buf)) > 0) {
}
}
// Create a TCP loopback socket pair. Returns the write-side Socket
// (wrapped for ESPHome) and the raw read-side fd for draining.
// Both ends are non-blocking with 16MB buffers.
inline std::pair<std::unique_ptr<socket::Socket>, int> create_tcp_loopback() {
// Create a TCP listener on loopback
int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr {};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = 0; // OS-assigned port
::bind(listen_fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr));
::listen(listen_fd, 1);
// Get the assigned port
socklen_t addr_len = sizeof(addr);
::getsockname(listen_fd, reinterpret_cast<struct sockaddr *>(&addr), &addr_len);
// Connect from client side
int write_fd = ::socket(AF_INET, SOCK_STREAM, 0);
::connect(write_fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr));
// Accept on server side (this is our read fd)
int read_fd = ::accept(listen_fd, nullptr, nullptr);
::close(listen_fd);
// Make both ends non-blocking
int flags = ::fcntl(write_fd, F_GETFL, 0);
::fcntl(write_fd, F_SETFL, flags | O_NONBLOCK);
flags = ::fcntl(read_fd, F_GETFL, 0);
::fcntl(read_fd, F_SETFL, flags | O_NONBLOCK);
// Use large socket buffers so benchmarks never hit WOULD_BLOCK
// during a single outer iteration (2000 × ~15B messages = ~30KB).
int bufsize = 16 * 1024 * 1024;
::setsockopt(write_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
::setsockopt(read_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
return {std::make_unique<socket::Socket>(write_fd), read_fd};
}
} // namespace esphome::api::benchmarks
@@ -0,0 +1,235 @@
#include <benchmark/benchmark.h>
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_buffer.h"
#include "esphome/components/light/color_mode.h"
namespace esphome::api::benchmarks {
static constexpr int kInnerIterations = 2000;
// --- ListEntitiesSensorResponse ---
static ListEntitiesSensorResponse make_sensor_response() {
ListEntitiesSensorResponse msg;
msg.object_id = StringRef::from_lit("living_room_temperature");
msg.key = 0x12345678;
msg.name = StringRef::from_lit("Living Room Temperature");
#ifdef USE_ENTITY_ICON
msg.icon = StringRef::from_lit("mdi:thermometer");
#endif
msg.entity_category = enums::ENTITY_CATEGORY_NONE;
msg.disabled_by_default = false;
msg.unit_of_measurement = StringRef::from_lit("°C");
msg.accuracy_decimals = 1;
msg.force_update = false;
msg.device_class = StringRef::from_lit("temperature");
msg.state_class = enums::STATE_CLASS_MEASUREMENT;
#ifdef USE_DEVICES
msg.device_id = 1;
#endif
return msg;
}
static void CalculateSize_ListEntitiesSensorResponse(benchmark::State &state) {
auto msg = make_sensor_response();
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result += msg.calculate_size();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalculateSize_ListEntitiesSensorResponse);
static void Encode_ListEntitiesSensorResponse(benchmark::State &state) {
auto msg = make_sensor_response();
APIBuffer buffer;
uint32_t size = msg.calculate_size();
buffer.resize(size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_ListEntitiesSensorResponse);
static void CalcAndEncode_ListEntitiesSensorResponse(benchmark::State &state) {
auto msg = make_sensor_response();
APIBuffer buffer;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalcAndEncode_ListEntitiesSensorResponse);
// --- ListEntitiesBinarySensorResponse ---
static ListEntitiesBinarySensorResponse make_binary_sensor_response() {
ListEntitiesBinarySensorResponse msg;
msg.object_id = StringRef::from_lit("front_door_contact");
msg.key = 0xAABBCCDD;
msg.name = StringRef::from_lit("Front Door Contact");
#ifdef USE_ENTITY_ICON
msg.icon = StringRef::from_lit("mdi:door");
#endif
msg.entity_category = enums::ENTITY_CATEGORY_NONE;
msg.disabled_by_default = false;
msg.device_class = StringRef::from_lit("door");
msg.is_status_binary_sensor = false;
#ifdef USE_DEVICES
msg.device_id = 2;
#endif
return msg;
}
static void CalculateSize_ListEntitiesBinarySensorResponse(benchmark::State &state) {
auto msg = make_binary_sensor_response();
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result += msg.calculate_size();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalculateSize_ListEntitiesBinarySensorResponse);
static void Encode_ListEntitiesBinarySensorResponse(benchmark::State &state) {
auto msg = make_binary_sensor_response();
APIBuffer buffer;
uint32_t size = msg.calculate_size();
buffer.resize(size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_ListEntitiesBinarySensorResponse);
static void CalcAndEncode_ListEntitiesBinarySensorResponse(benchmark::State &state) {
auto msg = make_binary_sensor_response();
APIBuffer buffer;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalcAndEncode_ListEntitiesBinarySensorResponse);
// --- ListEntitiesLightResponse ---
static light::ColorModeMask light_color_modes;
static FixedVector<const char *> light_effects;
static ListEntitiesLightResponse make_light_response() {
// Initialize static data on first call
static bool initialized = false;
if (!initialized) {
light_color_modes.insert(light::ColorMode::RGB_WHITE);
light_color_modes.insert(light::ColorMode::COLOR_TEMPERATURE);
light_effects.init(3);
light_effects.push_back("None");
light_effects.push_back("Rainbow");
light_effects.push_back("Strobe");
initialized = true;
}
ListEntitiesLightResponse msg;
msg.object_id = StringRef::from_lit("kitchen_ceiling_light");
msg.key = 0x55667788;
msg.name = StringRef::from_lit("Kitchen Ceiling Light");
#ifdef USE_ENTITY_ICON
msg.icon = StringRef::from_lit("mdi:ceiling-light");
#endif
msg.entity_category = enums::ENTITY_CATEGORY_NONE;
msg.disabled_by_default = false;
msg.supported_color_modes = &light_color_modes;
msg.min_mireds = 153.0f;
msg.max_mireds = 500.0f;
msg.effects = &light_effects;
#ifdef USE_DEVICES
msg.device_id = 3;
#endif
return msg;
}
static void CalculateSize_ListEntitiesLightResponse(benchmark::State &state) {
auto msg = make_light_response();
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result += msg.calculate_size();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalculateSize_ListEntitiesLightResponse);
static void Encode_ListEntitiesLightResponse(benchmark::State &state) {
auto msg = make_light_response();
APIBuffer buffer;
uint32_t size = msg.calculate_size();
buffer.resize(size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_ListEntitiesLightResponse);
static void CalcAndEncode_ListEntitiesLightResponse(benchmark::State &state) {
auto msg = make_light_response();
APIBuffer buffer;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalcAndEncode_ListEntitiesLightResponse);
} // namespace esphome::api::benchmarks
@@ -0,0 +1,118 @@
#include <benchmark/benchmark.h>
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_buffer.h"
namespace esphome::api::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
// Typical log line: "[12:34:56][D][sensor:094]: 'Temperature': Sending state 23.50000 with 1 decimals of accuracy"
static constexpr const char *kTypicalLogLine =
"[12:34:56][D][sensor:094]: 'Temperature': Sending state 23.50000 with 1 decimals of accuracy";
// Short log line: "[12:34:56][I][app:029]: Running..."
static constexpr const char *kShortLogLine = "[12:34:56][I][app:029]: Running...";
// --- Encode ---
static void Encode_LogResponse_Typical(benchmark::State &state) {
APIBuffer buffer;
SubscribeLogsResponse msg;
msg.level = enums::LOG_LEVEL_DEBUG;
msg.set_message(reinterpret_cast<const uint8_t *>(kTypicalLogLine), strlen(kTypicalLogLine));
uint32_t size = msg.calculate_size();
buffer.resize(size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_LogResponse_Typical);
static void Encode_LogResponse_Short(benchmark::State &state) {
APIBuffer buffer;
SubscribeLogsResponse msg;
msg.level = enums::LOG_LEVEL_INFO;
msg.set_message(reinterpret_cast<const uint8_t *>(kShortLogLine), strlen(kShortLogLine));
uint32_t size = msg.calculate_size();
buffer.resize(size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_LogResponse_Short);
// --- Calculate Size ---
static void CalculateSize_LogResponse_Typical(benchmark::State &state) {
SubscribeLogsResponse msg;
msg.level = enums::LOG_LEVEL_DEBUG;
msg.set_message(reinterpret_cast<const uint8_t *>(kTypicalLogLine), strlen(kTypicalLogLine));
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result += msg.calculate_size();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalculateSize_LogResponse_Typical);
// --- Calc + Encode (steady state) ---
static void CalcAndEncode_LogResponse_Typical(benchmark::State &state) {
APIBuffer buffer;
SubscribeLogsResponse msg;
msg.level = enums::LOG_LEVEL_DEBUG;
msg.set_message(reinterpret_cast<const uint8_t *>(kTypicalLogLine), strlen(kTypicalLogLine));
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalcAndEncode_LogResponse_Typical);
// --- Calc + Encode (fresh allocation each time) ---
static void CalcAndEncode_LogResponse_Typical_Fresh(benchmark::State &state) {
SubscribeLogsResponse msg;
msg.level = enums::LOG_LEVEL_DEBUG;
msg.set_message(reinterpret_cast<const uint8_t *>(kTypicalLogLine), strlen(kTypicalLogLine));
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
APIBuffer buffer;
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
benchmark::DoNotOptimize(buffer.data());
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalcAndEncode_LogResponse_Typical_Fresh);
} // namespace esphome::api::benchmarks
@@ -0,0 +1,306 @@
#include "esphome/core/defines.h"
#ifdef USE_API_NOISE
#include <benchmark/benchmark.h>
#include <cstring>
#include <memory>
#include "noise/protocol.h"
namespace esphome::api::benchmarks {
static constexpr int kInnerIterations = 2000;
// Helper to create and initialize a NoiseCipherState with ChaChaPoly.
// Returns nullptr on failure.
static NoiseCipherState *create_cipher() {
NoiseCipherState *cipher = nullptr;
int err = noise_cipherstate_new_by_id(&cipher, NOISE_CIPHER_CHACHAPOLY);
if (err != NOISE_ERROR_NONE || cipher == nullptr)
return nullptr;
// Initialize with a dummy 32-byte key (same pattern as handshake split produces)
uint8_t key[32];
memset(key, 0xAB, sizeof(key));
err = noise_cipherstate_init_key(cipher, key, sizeof(key));
if (err != NOISE_ERROR_NONE) {
noise_cipherstate_free(cipher);
return nullptr;
}
return cipher;
}
// Benchmark helper matching the exact pattern from
// APINoiseFrameHelper::write_protobuf_messages:
// - noise_buffer_init + noise_buffer_set_inout (same as production)
// - No explicit set_nonce (production relies on internal nonce increment)
// - Error checking on encrypt return
static void noise_encrypt_bench(benchmark::State &state, size_t plaintext_size) {
NoiseCipherState *cipher = create_cipher();
if (cipher == nullptr) {
state.SkipWithError("Failed to create cipher state");
return;
}
size_t mac_len = noise_cipherstate_get_mac_length(cipher);
size_t buf_capacity = plaintext_size + mac_len;
auto buffer = std::make_unique<uint8_t[]>(buf_capacity);
memset(buffer.get(), 0x42, plaintext_size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
// Match production: init buffer, set inout, encrypt
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buffer.get(), plaintext_size, buf_capacity);
int err = noise_cipherstate_encrypt(cipher, &mbuf);
if (err != NOISE_ERROR_NONE) {
state.SkipWithError("noise_cipherstate_encrypt failed");
noise_cipherstate_free(cipher);
return;
}
}
benchmark::DoNotOptimize(buffer[0]);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
noise_cipherstate_free(cipher);
}
// --- Encrypt a typical sensor state message (small payload ~14 bytes) ---
// This is the most common message encrypted on every sensor update.
// 4 bytes type+len header + ~10 bytes payload.
static void NoiseEncrypt_SmallMessage(benchmark::State &state) { noise_encrypt_bench(state, 14); }
BENCHMARK(NoiseEncrypt_SmallMessage);
// --- Encrypt a medium message (~128 bytes, typical for LightStateResponse) ---
static void NoiseEncrypt_MediumMessage(benchmark::State &state) { noise_encrypt_bench(state, 128); }
BENCHMARK(NoiseEncrypt_MediumMessage);
// --- Encrypt a large message (~1024 bytes, typical for DeviceInfoResponse) ---
static void NoiseEncrypt_LargeMessage(benchmark::State &state) { noise_encrypt_bench(state, 1024); }
BENCHMARK(NoiseEncrypt_LargeMessage);
// Benchmark helper matching the exact pattern from
// APINoiseFrameHelper::read_packet:
// - noise_buffer_init + noise_buffer_set_inout with capacity == size (decrypt shrinks)
// - Error checking on decrypt return
//
// Pre-encrypts kInnerIterations messages with sequential nonces before the
// timed loop. Each outer iteration re-inits the decrypt key to reset the
// nonce back to 0, then decrypts all pre-encrypted messages in sequence.
// The init_key cost is amortized over kInnerIterations decrypts.
static void noise_decrypt_bench(benchmark::State &state, size_t plaintext_size) {
NoiseCipherState *encrypt_cipher = create_cipher();
NoiseCipherState *decrypt_cipher = create_cipher();
if (encrypt_cipher == nullptr || decrypt_cipher == nullptr) {
state.SkipWithError("Failed to create cipher state");
if (encrypt_cipher)
noise_cipherstate_free(encrypt_cipher);
if (decrypt_cipher)
noise_cipherstate_free(decrypt_cipher);
return;
}
size_t mac_len = noise_cipherstate_get_mac_length(encrypt_cipher);
size_t encrypted_size = plaintext_size + mac_len;
// Pre-encrypt kInnerIterations messages with sequential nonces (0..N-1).
auto ciphertexts = std::make_unique<uint8_t[]>(encrypted_size * kInnerIterations);
for (int i = 0; i < kInnerIterations; i++) {
uint8_t *ct = ciphertexts.get() + i * encrypted_size;
memset(ct, 0x42, plaintext_size);
NoiseBuffer enc_buf;
noise_buffer_init(enc_buf);
noise_buffer_set_inout(enc_buf, ct, plaintext_size, encrypted_size);
int err = noise_cipherstate_encrypt(encrypt_cipher, &enc_buf);
if (err != NOISE_ERROR_NONE) {
state.SkipWithError("Pre-encrypt failed");
noise_cipherstate_free(encrypt_cipher);
noise_cipherstate_free(decrypt_cipher);
return;
}
}
// Working buffer — decrypt modifies in place
auto buffer = std::make_unique<uint8_t[]>(encrypted_size);
static constexpr uint8_t KEY[32] = {0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB,
0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB,
0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB};
for (auto _ : state) {
// Reset nonce to 0 by re-initing the key (amortized over kInnerIterations)
noise_cipherstate_init_key(decrypt_cipher, KEY, sizeof(KEY));
for (int i = 0; i < kInnerIterations; i++) {
// Copy ciphertext into working buffer (decrypt modifies in place)
memcpy(buffer.get(), ciphertexts.get() + i * encrypted_size, encrypted_size);
// Decrypt matching production pattern
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buffer.get(), encrypted_size, encrypted_size);
int err = noise_cipherstate_decrypt(decrypt_cipher, &mbuf);
if (err != NOISE_ERROR_NONE) {
state.SkipWithError("noise_cipherstate_decrypt failed");
noise_cipherstate_free(encrypt_cipher);
noise_cipherstate_free(decrypt_cipher);
return;
}
}
benchmark::DoNotOptimize(buffer[0]);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
noise_cipherstate_free(encrypt_cipher);
noise_cipherstate_free(decrypt_cipher);
}
// --- Decrypt benchmarks (matching read_packet path) ---
static void NoiseDecrypt_SmallMessage(benchmark::State &state) { noise_decrypt_bench(state, 14); }
BENCHMARK(NoiseDecrypt_SmallMessage);
static void NoiseDecrypt_MediumMessage(benchmark::State &state) { noise_decrypt_bench(state, 128); }
BENCHMARK(NoiseDecrypt_MediumMessage);
static void NoiseDecrypt_LargeMessage(benchmark::State &state) { noise_decrypt_bench(state, 1024); }
BENCHMARK(NoiseDecrypt_LargeMessage);
// --- Full Noise_NNpsk0 handshake benchmark ---
// Measures the complete handshake between initiator and responder:
// - Create handshake states for both sides
// - Set PSK and prologue
// - Exchange messages (initiator write -> responder read -> responder write -> initiator read)
// - Split to get cipher states
// This is dominated by Curve25519 DH operations (expensive on ESP8266).
// No inner iterations — each handshake is already expensive enough.
static void NoiseHandshake_Full(benchmark::State &state) {
// Matching ESPHome's protocol: Noise_NNpsk0_25519_ChaChaPoly_SHA256
NoiseProtocolId nid;
memset(&nid, 0, sizeof(nid));
nid.pattern_id = NOISE_PATTERN_NN;
nid.cipher_id = NOISE_CIPHER_CHACHAPOLY;
nid.dh_id = NOISE_DH_CURVE25519;
nid.prefix_id = NOISE_PREFIX_STANDARD;
nid.hybrid_id = NOISE_DH_NONE;
nid.hash_id = NOISE_HASH_SHA256;
nid.modifier_ids[0] = NOISE_MODIFIER_PSK0;
// Dummy PSK (32 bytes) and prologue matching production setup
static constexpr uint8_t PSK[32] = {0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB,
0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB,
0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB};
static constexpr uint8_t PROLOGUE[] = "NoESPHome";
// Message buffer for handshake exchange (max handshake message ~96 bytes)
uint8_t msg_buf[128];
for (auto _ : state) {
NoiseHandshakeState *initiator = nullptr;
NoiseHandshakeState *responder = nullptr;
NoiseCipherState *init_send = nullptr, *init_recv = nullptr;
NoiseCipherState *resp_send = nullptr, *resp_recv = nullptr;
int err;
// Create both handshake states
err = noise_handshakestate_new_by_id(&initiator, &nid, NOISE_ROLE_INITIATOR);
if (err != NOISE_ERROR_NONE) {
state.SkipWithError("Failed to create initiator");
return;
}
err = noise_handshakestate_new_by_id(&responder, &nid, NOISE_ROLE_RESPONDER);
if (err != NOISE_ERROR_NONE) {
state.SkipWithError("Failed to create responder");
noise_handshakestate_free(initiator);
return;
}
// Set PSK and prologue on both sides
noise_handshakestate_set_pre_shared_key(initiator, PSK, sizeof(PSK));
noise_handshakestate_set_pre_shared_key(responder, PSK, sizeof(PSK));
noise_handshakestate_set_prologue(initiator, PROLOGUE, sizeof(PROLOGUE) - 1);
noise_handshakestate_set_prologue(responder, PROLOGUE, sizeof(PROLOGUE) - 1);
noise_handshakestate_start(initiator);
noise_handshakestate_start(responder);
// Message 1: Initiator -> Responder
NoiseBuffer write_buf, read_buf;
noise_buffer_set_output(write_buf, msg_buf, sizeof(msg_buf));
err = noise_handshakestate_write_message(initiator, &write_buf, nullptr);
if (err != NOISE_ERROR_NONE) {
state.SkipWithError("Initiator write_message failed");
noise_handshakestate_free(initiator);
noise_handshakestate_free(responder);
return;
}
noise_buffer_set_input(read_buf, msg_buf, write_buf.size);
err = noise_handshakestate_read_message(responder, &read_buf, nullptr);
if (err != NOISE_ERROR_NONE) {
state.SkipWithError("Responder read_message failed");
noise_handshakestate_free(initiator);
noise_handshakestate_free(responder);
return;
}
// Message 2: Responder -> Initiator
noise_buffer_set_output(write_buf, msg_buf, sizeof(msg_buf));
err = noise_handshakestate_write_message(responder, &write_buf, nullptr);
if (err != NOISE_ERROR_NONE) {
state.SkipWithError("Responder write_message failed");
noise_handshakestate_free(initiator);
noise_handshakestate_free(responder);
return;
}
noise_buffer_set_input(read_buf, msg_buf, write_buf.size);
err = noise_handshakestate_read_message(initiator, &read_buf, nullptr);
if (err != NOISE_ERROR_NONE) {
state.SkipWithError("Initiator read_message failed");
noise_handshakestate_free(initiator);
noise_handshakestate_free(responder);
return;
}
// Split to get cipher states
err = noise_handshakestate_split(initiator, &init_send, &init_recv);
if (err != NOISE_ERROR_NONE) {
state.SkipWithError("Initiator split failed");
noise_handshakestate_free(initiator);
noise_handshakestate_free(responder);
return;
}
err = noise_handshakestate_split(responder, &resp_send, &resp_recv);
if (err != NOISE_ERROR_NONE) {
state.SkipWithError("Responder split failed");
noise_handshakestate_free(initiator);
noise_handshakestate_free(responder);
noise_cipherstate_free(init_send);
noise_cipherstate_free(init_recv);
return;
}
benchmark::DoNotOptimize(init_send);
// Cleanup
noise_handshakestate_free(initiator);
noise_handshakestate_free(responder);
noise_cipherstate_free(init_send);
noise_cipherstate_free(init_recv);
noise_cipherstate_free(resp_send);
noise_cipherstate_free(resp_recv);
}
}
BENCHMARK(NoiseHandshake_Full);
} // namespace esphome::api::benchmarks
#endif // USE_API_NOISE
@@ -0,0 +1,108 @@
#include "esphome/core/defines.h"
#ifdef USE_API_PLAINTEXT
#include <benchmark/benchmark.h>
#include <unistd.h>
#include "bench_helpers.h"
#include "esphome/components/api/api_frame_helper_plaintext.h"
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_buffer.h"
namespace esphome::api::benchmarks {
static constexpr int kInnerIterations = 2000;
// Helper to create a TCP loopback connection with an APIPlaintextFrameHelper
// on the write end. Returns the helper and the read-side fd.
static std::pair<std::unique_ptr<APIPlaintextFrameHelper>, int> create_plaintext_helper() {
auto [sock, read_fd] = create_tcp_loopback();
auto helper = std::make_unique<APIPlaintextFrameHelper>(std::move(sock));
helper->init();
return {std::move(helper), read_fd};
}
// --- Write a single SensorStateResponse through plaintext framing ---
// Measures the full write path: header construction, varint encoding,
// iovec assembly, and socket write.
static void PlaintextFrame_WriteSensorState(benchmark::State &state) {
auto [helper, read_fd] = create_plaintext_helper();
uint8_t padding = helper->frame_header_padding();
// Pre-init buffer to typical TCP MSS size to avoid benchmarking
// heap allocation — in real use the buffer is reused across writes.
APIBuffer buffer;
buffer.reserve(1460);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
buffer.clear();
SensorStateResponse msg;
msg.key = 0x12345678;
msg.state = 23.5f;
msg.missing_state = false;
uint32_t size = msg.calculate_size();
buffer.resize(padding + size);
ProtoWriteBuffer writer(&buffer, padding);
msg.encode(writer);
helper->write_protobuf_packet(SensorStateResponse::MESSAGE_TYPE, writer);
}
drain_socket(read_fd);
benchmark::DoNotOptimize(helper.get());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
::close(read_fd);
}
BENCHMARK(PlaintextFrame_WriteSensorState);
// --- Write a batch of 5 SensorStateResponses in one call ---
// Measures batched write: multiple messages assembled into one writev.
static void PlaintextFrame_WriteBatch5(benchmark::State &state) {
auto [helper, read_fd] = create_plaintext_helper();
uint8_t padding = helper->frame_header_padding();
uint8_t footer = helper->frame_footer_size();
// Pre-init buffer to typical TCP MSS size to avoid benchmarking
// heap allocation — in real use the buffer is reused across writes.
APIBuffer buffer;
buffer.reserve(1460);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
buffer.clear();
MessageInfo messages[5] = {{0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}};
for (int j = 0; j < 5; j++) {
uint16_t offset = buffer.size();
SensorStateResponse msg;
msg.key = static_cast<uint32_t>(j);
msg.state = 23.5f + static_cast<float>(j);
msg.missing_state = false;
uint32_t size = msg.calculate_size();
buffer.resize(offset + padding + size + footer);
ProtoWriteBuffer writer(&buffer, offset + padding);
msg.encode(writer);
messages[j] = MessageInfo(SensorStateResponse::MESSAGE_TYPE, offset, size, padding);
}
helper->write_protobuf_messages(ProtoWriteBuffer(&buffer, 0), std::span<const MessageInfo>(messages, 5));
}
drain_socket(read_fd);
benchmark::DoNotOptimize(helper.get());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
::close(read_fd);
}
BENCHMARK(PlaintextFrame_WriteBatch5);
} // namespace esphome::api::benchmarks
#endif // USE_API_PLAINTEXT
@@ -0,0 +1,113 @@
#include <benchmark/benchmark.h>
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_buffer.h"
namespace esphome::api::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
// sub-microsecond benchmarks.
static constexpr int kInnerIterations = 2000;
// Helper: encode a message into an APIBuffer for reuse in decode benchmarks.
// Optimization barriers are applied to the decode target objects via
// DoNotOptimize/ClobberMemory, not to this buffer.
template<typename T> static APIBuffer encode_message(const T &msg) {
APIBuffer buffer;
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
return buffer;
}
/// Force a pointer through an asm barrier so the compiler cannot
/// prove its contents are unchanged across iterations.
/// benchmark::DoNotOptimize/ClobberMemory are insufficient under
/// CodSpeed's valgrind-based instrumentation.
static void escape(void *p) { asm volatile("" : : "g"(p) : "memory"); }
// --- HelloRequest decode (string + varint fields) ---
static void Decode_HelloRequest(benchmark::State &state) {
HelloRequest source;
source.client_info = StringRef::from_lit("aioesphomeapi");
source.api_version_major = 1;
source.api_version_minor = 10;
auto encoded = encode_message(source);
auto *data = encoded.data();
auto size = encoded.size();
benchmark::DoNotOptimize(data);
benchmark::DoNotOptimize(size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
HelloRequest msg;
escape(&msg);
msg.decode(data, size);
escape(&msg);
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Decode_HelloRequest);
// --- SwitchCommandRequest decode (simple command) ---
static void Decode_SwitchCommandRequest(benchmark::State &state) {
SwitchCommandRequest source;
source.key = 0x12345678;
source.state = true;
auto encoded = encode_message(source);
auto *data = encoded.data();
auto size = encoded.size();
benchmark::DoNotOptimize(data);
benchmark::DoNotOptimize(size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
SwitchCommandRequest msg;
escape(&msg);
msg.decode(data, size);
escape(&msg);
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Decode_SwitchCommandRequest);
// --- LightCommandRequest decode (complex command with many fields) ---
static void Decode_LightCommandRequest(benchmark::State &state) {
LightCommandRequest source;
source.key = 0x11223344;
source.has_state = true;
source.state = true;
source.has_brightness = true;
source.brightness = 0.8f;
source.has_rgb = true;
source.red = 1.0f;
source.green = 0.5f;
source.blue = 0.2f;
source.has_effect = true;
source.effect = StringRef::from_lit("rainbow");
auto encoded = encode_message(source);
auto *data = encoded.data();
auto size = encoded.size();
benchmark::DoNotOptimize(data);
benchmark::DoNotOptimize(size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
LightCommandRequest msg;
escape(&msg);
msg.decode(data, size);
escape(&msg);
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Decode_LightCommandRequest);
} // namespace esphome::api::benchmarks
@@ -0,0 +1,387 @@
#include <benchmark/benchmark.h>
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_buffer.h"
namespace esphome::api::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
// sub-microsecond benchmarks.
static constexpr int kInnerIterations = 2000;
// --- SensorStateResponse (highest frequency message) ---
static void Encode_SensorStateResponse(benchmark::State &state) {
APIBuffer buffer;
SensorStateResponse msg;
msg.key = 0x12345678;
msg.state = 23.5f;
msg.missing_state = false;
uint32_t size = msg.calculate_size();
buffer.resize(size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_SensorStateResponse);
static void CalculateSize_SensorStateResponse(benchmark::State &state) {
SensorStateResponse msg;
msg.key = 0x12345678;
msg.state = 23.5f;
msg.missing_state = false;
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result += msg.calculate_size();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalculateSize_SensorStateResponse);
// Steady state: buffer already allocated from previous iteration
static void CalcAndEncode_SensorStateResponse(benchmark::State &state) {
APIBuffer buffer;
SensorStateResponse msg;
msg.key = 0x12345678;
msg.state = 23.5f;
msg.missing_state = false;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalcAndEncode_SensorStateResponse);
// Cold path: fresh buffer each iteration (measures heap allocation cost).
// Inner loop still needed to amortize CodSpeed instrumentation overhead.
// Each inner iteration creates a fresh buffer, so this measures
// alloc+calc+encode per item.
static void CalcAndEncode_SensorStateResponse_Fresh(benchmark::State &state) {
SensorStateResponse msg;
msg.key = 0x12345678;
msg.state = 23.5f;
msg.missing_state = false;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
APIBuffer buffer;
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
benchmark::DoNotOptimize(buffer.data());
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalcAndEncode_SensorStateResponse_Fresh);
// --- BinarySensorStateResponse ---
static void Encode_BinarySensorStateResponse(benchmark::State &state) {
APIBuffer buffer;
BinarySensorStateResponse msg;
msg.key = 0xAABBCCDD;
msg.state = true;
msg.missing_state = false;
uint32_t size = msg.calculate_size();
buffer.resize(size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_BinarySensorStateResponse);
// --- HelloResponse (string fields) ---
static void Encode_HelloResponse(benchmark::State &state) {
APIBuffer buffer;
HelloResponse msg;
msg.api_version_major = 1;
msg.api_version_minor = 10;
msg.server_info = StringRef::from_lit("esphome v2026.3.0");
msg.name = StringRef::from_lit("living-room-sensor");
uint32_t size = msg.calculate_size();
buffer.resize(size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_HelloResponse);
// --- LightStateResponse (complex multi-field message) ---
static void Encode_LightStateResponse(benchmark::State &state) {
APIBuffer buffer;
LightStateResponse msg;
msg.key = 0x11223344;
msg.state = true;
msg.brightness = 0.8f;
msg.color_mode = enums::COLOR_MODE_RGB_WHITE;
msg.color_brightness = 1.0f;
msg.red = 1.0f;
msg.green = 0.5f;
msg.blue = 0.2f;
msg.white = 0.0f;
msg.color_temperature = 4000.0f;
msg.cold_white = 0.0f;
msg.warm_white = 0.0f;
msg.effect = StringRef::from_lit("rainbow");
uint32_t size = msg.calculate_size();
buffer.resize(size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_LightStateResponse);
static void CalculateSize_LightStateResponse(benchmark::State &state) {
LightStateResponse msg;
msg.key = 0x11223344;
msg.state = true;
msg.brightness = 0.8f;
msg.color_mode = enums::COLOR_MODE_RGB_WHITE;
msg.color_brightness = 1.0f;
msg.red = 1.0f;
msg.green = 0.5f;
msg.blue = 0.2f;
msg.white = 0.0f;
msg.color_temperature = 4000.0f;
msg.cold_white = 0.0f;
msg.warm_white = 0.0f;
msg.effect = StringRef::from_lit("rainbow");
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result += msg.calculate_size();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalculateSize_LightStateResponse);
// --- DeviceInfoResponse (nested submessages: 20 devices + 20 areas) ---
static DeviceInfoResponse make_device_info_response() {
DeviceInfoResponse msg;
msg.name = StringRef::from_lit("living-room-sensor");
msg.mac_address = StringRef::from_lit("AA:BB:CC:DD:EE:FF");
msg.esphome_version = StringRef::from_lit("2026.3.0");
msg.compilation_time = StringRef::from_lit("Mar 16 2026, 12:00:00");
msg.model = StringRef::from_lit("esp32-poe-iso");
msg.manufacturer = StringRef::from_lit("Olimex");
msg.friendly_name = StringRef::from_lit("Living Room Sensor");
#ifdef USE_DEVICES
for (uint32_t i = 0; i < ESPHOME_DEVICE_COUNT && i < 20; i++) {
msg.devices[i].device_id = i + 1;
msg.devices[i].name = StringRef::from_lit("device");
msg.devices[i].area_id = (i % 20) + 1;
}
#endif
#ifdef USE_AREAS
for (uint32_t i = 0; i < ESPHOME_AREA_COUNT && i < 20; i++) {
msg.areas[i].area_id = i + 1;
msg.areas[i].name = StringRef::from_lit("area");
}
#endif
return msg;
}
static void CalculateSize_DeviceInfoResponse(benchmark::State &state) {
auto msg = make_device_info_response();
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result += msg.calculate_size();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalculateSize_DeviceInfoResponse);
static void Encode_DeviceInfoResponse(benchmark::State &state) {
auto msg = make_device_info_response();
APIBuffer buffer;
uint32_t total_size = msg.calculate_size();
buffer.resize(total_size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_DeviceInfoResponse);
// Steady state: buffer already allocated from previous iteration
static void CalcAndEncode_DeviceInfoResponse(benchmark::State &state) {
auto msg = make_device_info_response();
APIBuffer buffer;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalcAndEncode_DeviceInfoResponse);
// Cold path: fresh buffer each iteration (measures heap allocation cost).
// Inner loop still needed to amortize CodSpeed instrumentation overhead.
// Each inner iteration creates a fresh buffer, so this measures
// alloc+calc+encode per item.
static void CalcAndEncode_DeviceInfoResponse_Fresh(benchmark::State &state) {
auto msg = make_device_info_response();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
APIBuffer buffer;
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
benchmark::DoNotOptimize(buffer.data());
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalcAndEncode_DeviceInfoResponse_Fresh);
// --- BluetoothLERawAdvertisementsResponse (12 adverts, highest-volume BLE message) ---
#ifdef USE_BLUETOOTH_PROXY
static BluetoothLERawAdvertisementsResponse make_ble_raw_advs_12() {
static const uint8_t fake_adv_data[] = {
0x02, 0x01, 0x06, 0x03, 0x03, 0x9F, 0xFE, 0x17, 0x16, 0x9F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
BluetoothLERawAdvertisementsResponse msg;
msg.advertisements_len = 12;
for (int i = 0; i < 12; i++) {
auto &adv = msg.advertisements[i];
adv.address = 0xAABBCCDD0000ULL + i;
adv.rssi = -60 - i;
adv.address_type = 1;
memcpy(adv.data, fake_adv_data, sizeof(fake_adv_data));
adv.data_len = sizeof(fake_adv_data);
}
return msg;
}
static void CalculateSize_BLERawAdvs12(benchmark::State &state) {
auto msg = make_ble_raw_advs_12();
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result += msg.calculate_size();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalculateSize_BLERawAdvs12);
static void Encode_BLERawAdvs12(benchmark::State &state) {
auto msg = make_ble_raw_advs_12();
APIBuffer buffer;
uint32_t total_size = msg.calculate_size();
buffer.resize(total_size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_BLERawAdvs12);
static void CalcAndEncode_BLERawAdvs12(benchmark::State &state) {
auto msg = make_ble_raw_advs_12();
APIBuffer buffer;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalcAndEncode_BLERawAdvs12);
static void CalcAndEncode_BLERawAdvs12_Fresh(benchmark::State &state) {
auto msg = make_ble_raw_advs_12();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
APIBuffer buffer;
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
benchmark::DoNotOptimize(buffer.data());
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalcAndEncode_BLERawAdvs12_Fresh);
#endif // USE_BLUETOOTH_PROXY
} // namespace esphome::api::benchmarks
@@ -0,0 +1,280 @@
// Encode/decode microbenchmarks for proxy message families that carry
// high-volume traffic (Z-Wave, IR/RF, serial). Mirrors the existing
// BluetoothLERawAdvertisementsResponse benchmarks in bench_proto_encode.cpp.
#include <benchmark/benchmark.h>
#include <cstring>
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_buffer.h"
namespace esphome::api::benchmarks {
static constexpr int kInnerIterations = 2000;
// Encodes `src` into `out`. Caller owns `out` and must keep it alive across
// the decode loop (decoded messages may store pointers back into its bytes).
template<typename T> static void encode_into(APIBuffer &out, const T &src) {
out.resize(src.calculate_size());
ProtoWriteBuffer writer(&out, 0);
src.encode(writer);
}
// --- ZWaveProxyFrame (Z-Wave frame, ~16 bytes payload) ---
#ifdef USE_ZWAVE_PROXY
static const uint8_t kZWaveFrameData[] = {0x01, 0x09, 0x00, 0x13, 0x01, 0x02, 0x00, 0x00,
0x25, 0x00, 0x05, 0xC4, 0x00, 0x00, 0x00, 0x00};
static void Encode_ZWaveProxyFrame(benchmark::State &state) {
ZWaveProxyFrame msg;
msg.data = kZWaveFrameData;
msg.data_len = sizeof(kZWaveFrameData);
APIBuffer buffer;
buffer.resize(msg.calculate_size());
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_ZWaveProxyFrame);
static void Decode_ZWaveProxyFrame(benchmark::State &state) {
ZWaveProxyFrame source;
source.data = kZWaveFrameData;
source.data_len = sizeof(kZWaveFrameData);
APIBuffer encoded;
encode_into(encoded, source);
const uint8_t *data = encoded.data();
size_t size = encoded.size();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ZWaveProxyFrame msg;
msg.decode(data, size);
benchmark::DoNotOptimize(msg);
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Decode_ZWaveProxyFrame);
static const uint8_t kZWaveRequestData[] = {0xDE, 0xAD, 0xBE, 0xEF};
static void Decode_ZWaveProxyRequest(benchmark::State &state) {
ZWaveProxyRequest source;
source.type = enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE;
source.data = kZWaveRequestData;
source.data_len = sizeof(kZWaveRequestData);
APIBuffer encoded;
encode_into(encoded, source);
const uint8_t *data = encoded.data();
size_t size = encoded.size();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ZWaveProxyRequest msg;
msg.decode(data, size);
benchmark::DoNotOptimize(msg);
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Decode_ZWaveProxyRequest);
#endif // USE_ZWAVE_PROXY
// --- SerialProxyDataReceived encode + SerialProxyWriteRequest decode ---
//
// SerialProxyWriteRequest is decode-only (SOURCE_CLIENT) but has the same
// wire layout as SerialProxyDataReceived, so we encode via the latter and
// decode as the former.
#ifdef USE_SERIAL_PROXY
static constexpr size_t kSerialPayloadSize = 64;
static const uint8_t kSerialPayload[kSerialPayloadSize] = {
0x55, 0xAA, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB,
0xCD, 0xEF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
0xFF, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0,
0xF0, 0x0F, 0x1F, 0x2F, 0x3F, 0x4F, 0x5F, 0x6F, 0x7F, 0x8F, 0x9F, 0xAF, 0xBF, 0xCF, 0xDF, 0xEF};
static void Encode_SerialProxyDataReceived(benchmark::State &state) {
SerialProxyDataReceived msg;
msg.instance = 0;
msg.set_data(kSerialPayload, kSerialPayloadSize);
APIBuffer buffer;
buffer.resize(msg.calculate_size());
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_SerialProxyDataReceived);
static void Decode_SerialProxyWriteRequest(benchmark::State &state) {
SerialProxyDataReceived source;
source.instance = 0;
source.set_data(kSerialPayload, kSerialPayloadSize);
APIBuffer encoded;
encode_into(encoded, source);
const uint8_t *data = encoded.data();
size_t size = encoded.size();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
SerialProxyWriteRequest msg;
msg.decode(data, size);
benchmark::DoNotOptimize(msg);
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Decode_SerialProxyWriteRequest);
#endif // USE_SERIAL_PROXY
// --- InfraredRFReceiveEvent encode (100 sint32 timings) +
// InfraredRFTransmitRawTimingsRequest decode (hand-built wire bytes) ---
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
// Mark/space pairs simulating a typical RC-5 / NEC capture (100 timings).
static std::vector<int32_t> make_ir_timings_100() {
std::vector<int32_t> v;
v.reserve(100);
for (int i = 0; i < 100; i++) {
v.push_back((i % 2 == 0) ? 560 : -560);
}
return v;
}
static const std::vector<int32_t> &get_ir_timings_100() {
static const std::vector<int32_t> timings = make_ir_timings_100();
return timings;
}
static void Encode_InfraredRFReceiveEvent(benchmark::State &state) {
InfraredRFReceiveEvent msg;
msg.key = 0xDEADBEEF;
msg.timings = &get_ir_timings_100();
APIBuffer buffer;
buffer.resize(msg.calculate_size());
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_InfraredRFReceiveEvent);
static void CalculateSize_InfraredRFReceiveEvent(benchmark::State &state) {
InfraredRFReceiveEvent msg;
msg.key = 0xDEADBEEF;
msg.timings = &get_ir_timings_100();
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result += msg.calculate_size();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalculateSize_InfraredRFReceiveEvent);
// Hand-built wire bytes for InfraredRFTransmitRawTimingsRequest (decode-only,
// no sister message with identical wire layout).
// field 2 (key, fixed32): tag=0x15, 4 LE bytes
// field 3 (carrier_frequency): tag=0x18, varint
// field 4 (repeat_count): tag=0x20, varint
// field 5 (timings, packed sint32): tag=0x2A, length varint, packed payload
// field 6 (modulation): tag=0x30, varint
static APIBuffer build_infrared_rf_transmit_wire() {
uint8_t bytes[256];
size_t len = 0;
auto put_byte = [&](uint8_t b) { bytes[len++] = b; };
auto put_varint = [&](uint32_t v) {
while (v >= 0x80) {
bytes[len++] = static_cast<uint8_t>((v & 0x7F) | 0x80);
v >>= 7;
}
bytes[len++] = static_cast<uint8_t>(v);
};
auto encode_zigzag = [](int32_t v) -> uint32_t {
return (static_cast<uint32_t>(v) << 1) ^ static_cast<uint32_t>(v >> 31);
};
put_byte(0x15);
put_byte(0xEF);
put_byte(0xBE);
put_byte(0xAD);
put_byte(0xDE);
put_byte(0x18);
put_varint(38000);
put_byte(0x20);
put_varint(2);
uint8_t packed[200];
size_t packed_len = 0;
for (int i = 0; i < 100; i++) {
int32_t value = (i % 2 == 0) ? 560 : -560;
uint32_t zz = encode_zigzag(value);
while (zz >= 0x80) {
packed[packed_len++] = static_cast<uint8_t>((zz & 0x7F) | 0x80);
zz >>= 7;
}
packed[packed_len++] = static_cast<uint8_t>(zz);
}
put_byte(0x2A);
put_varint(static_cast<uint32_t>(packed_len));
std::memcpy(bytes + len, packed, packed_len);
len += packed_len;
// field 6: modulation = 1 (non-zero so it's actually emitted and exercises
// decode_varint for this field, matching the documented layout above).
put_byte(0x30);
put_varint(1);
APIBuffer buf;
buf.resize(len);
std::memcpy(buf.data(), bytes, len);
return buf;
}
static void Decode_InfraredRFTransmitRawTimingsRequest(benchmark::State &state) {
auto encoded = build_infrared_rf_transmit_wire();
const uint8_t *data = encoded.data();
size_t size = encoded.size();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
InfraredRFTransmitRawTimingsRequest msg;
msg.decode(data, size);
benchmark::DoNotOptimize(msg);
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Decode_InfraredRFTransmitRawTimingsRequest);
#endif // USE_IR_RF || USE_RADIO_FREQUENCY
} // namespace esphome::api::benchmarks
@@ -0,0 +1,133 @@
#include <benchmark/benchmark.h>
#include "esphome/components/api/proto.h"
#include "esphome/components/api/api_buffer.h"
namespace esphome::api::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
// sub-microsecond benchmarks.
static constexpr int kInnerIterations = 2000;
// --- ProtoVarInt::parse() benchmarks ---
static void ProtoVarInt_Parse_SingleByte(benchmark::State &state) {
uint8_t buf[] = {0x42}; // value = 66
for (auto _ : state) {
ProtoVarIntResult result{};
for (int i = 0; i < kInnerIterations; i++) {
result = ProtoVarInt::parse(buf, sizeof(buf));
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ProtoVarInt_Parse_SingleByte);
static void ProtoVarInt_Parse_TwoByte(benchmark::State &state) {
uint8_t buf[] = {0x80, 0x01}; // value = 128
for (auto _ : state) {
ProtoVarIntResult result{};
for (int i = 0; i < kInnerIterations; i++) {
result = ProtoVarInt::parse(buf, sizeof(buf));
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ProtoVarInt_Parse_TwoByte);
static void ProtoVarInt_Parse_FiveByte(benchmark::State &state) {
uint8_t buf[] = {0xFF, 0xFF, 0xFF, 0xFF, 0x0F};
for (auto _ : state) {
ProtoVarIntResult result{};
for (int i = 0; i < kInnerIterations; i++) {
result = ProtoVarInt::parse(buf, sizeof(buf));
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ProtoVarInt_Parse_FiveByte);
// --- Varint encoding benchmarks ---
static void Encode_Varint_Small(benchmark::State &state) {
APIBuffer buffer;
buffer.resize(16);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
writer.encode_varint_raw(42);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_Varint_Small);
static void Encode_Varint_Large(benchmark::State &state) {
APIBuffer buffer;
buffer.resize(16);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
writer.encode_varint_raw(300);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_Varint_Large);
static void Encode_Varint_MaxUint32(benchmark::State &state) {
APIBuffer buffer;
buffer.resize(16);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
writer.encode_varint_raw(0xFFFFFFFF);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_Varint_MaxUint32);
// --- ProtoSize::varint() benchmarks ---
static void ProtoSize_Varint_Small(benchmark::State &state) {
// Use varying input to prevent constant folding.
// Values 0-127 all take 1 byte but the compiler can't prove that.
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result += ProtoSize::varint(static_cast<uint32_t>(i) & 0x7F);
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ProtoSize_Varint_Small);
static void ProtoSize_Varint_Large(benchmark::State &state) {
// Use varying input to prevent constant folding.
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result += ProtoSize::varint(0xFFFF0000 | static_cast<uint32_t>(i));
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ProtoSize_Varint_Large);
} // namespace esphome::api::benchmarks
@@ -0,0 +1,191 @@
#include "esphome/core/defines.h"
#if defined(USE_API_PLAINTEXT) && defined(USE_SENSOR)
#include <benchmark/benchmark.h>
#include <unistd.h>
#include "bench_helpers.h"
#include "esphome/components/api/api_connection.h"
#include "esphome/components/api/api_server.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::api {
// Friend functions declared in APIConnection for benchmark access.
void bench_enable_immediate_send(APIConnection *conn) { conn->flags_.should_try_send_immediately = true; }
void bench_clear_batch(APIConnection *conn) { conn->clear_batch_(); }
void bench_process_batch(APIConnection *conn) { conn->process_batch_(); }
} // namespace esphome::api
namespace esphome::api::benchmarks {
static constexpr int kInnerIterations = 2000;
// Helper to create a TCP loopback connection with an APIConnection.
// Returns the connection and the read-side fd for draining.
static std::pair<std::unique_ptr<APIConnection>, int> create_api_connection() {
auto [sock, read_fd] = create_tcp_loopback();
auto conn = std::make_unique<APIConnection>(std::move(sock), global_api_server);
conn->start();
return {std::move(conn), read_fd};
}
// Test subclass to access protected configure_entity_() for benchmark setup.
class TestSensor : public sensor::Sensor {
public:
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
};
// --- send_sensor_state: immediate send path ---
// Measures: send_message_smart_ → prepare buffer → dispatch_message_ →
// try_send_sensor_state → fill key/device_id + proto encode → frame write →
// TCP send. This is the per-client cost when batch_delay=0 and initial states
// have been sent.
static void SendSensorState_Immediate(benchmark::State &state) {
auto [conn, read_fd] = create_api_connection();
bench_enable_immediate_send(conn.get());
// batch_delay must be 0 for should_send_immediately_ to return true
uint16_t saved_delay = global_api_server->get_batch_delay();
global_api_server->set_batch_delay(0);
TestSensor sensor;
sensor.configure("test_sensor");
sensor.publish_state(23.5f);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
conn->send_sensor_state(&sensor);
}
drain_socket(read_fd);
benchmark::DoNotOptimize(conn.get());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
global_api_server->set_batch_delay(saved_delay);
::close(read_fd);
}
BENCHMARK(SendSensorState_Immediate);
// --- send_sensor_state: batch path (cold — first call allocates) ---
// Measures: send_message_smart_ → schedule_message_ → deferred batch add.
// Includes one-time vector allocation cost.
static void SendSensorState_Batch_Cold(benchmark::State &state) {
auto [conn, read_fd] = create_api_connection();
TestSensor sensor;
sensor.configure("test_sensor");
sensor.publish_state(23.5f);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
conn->send_sensor_state(&sensor);
}
benchmark::DoNotOptimize(conn.get());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
::close(read_fd);
}
BENCHMARK(SendSensorState_Batch_Cold);
// --- send_sensor_state: batch path (warm — buffer already allocated) ---
// Measures steady-state batch cost after the vector has been allocated
// and cleared at least once. This is the typical path during normal
// operation after the first batch has been processed.
static void SendSensorState_Batch_Warm(benchmark::State &state) {
auto [conn, read_fd] = create_api_connection();
TestSensor sensor;
sensor.configure("test_sensor");
sensor.publish_state(23.5f);
// Warm up: send once to allocate, then clear to keep capacity
conn->send_sensor_state(&sensor);
bench_clear_batch(conn.get());
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
conn->send_sensor_state(&sensor);
}
benchmark::DoNotOptimize(conn.get());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
::close(read_fd);
}
BENCHMARK(SendSensorState_Batch_Warm);
// --- process_batch_: single sensor state (encode + frame + write) ---
// Measures the deferred batch processing path: dispatch_message_ →
// try_send_sensor_state → fill + proto encode → send_buffer → frame write.
// This is the cost paid on the next loop() after batching.
static void ProcessBatch_SingleSensor(benchmark::State &state) {
auto [conn, read_fd] = create_api_connection();
TestSensor sensor;
sensor.configure("test_sensor");
sensor.publish_state(23.5f);
// Warm up batch vector
conn->send_sensor_state(&sensor);
bench_process_batch(conn.get());
drain_socket(read_fd);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
conn->send_sensor_state(&sensor);
bench_process_batch(conn.get());
}
drain_socket(read_fd);
benchmark::DoNotOptimize(conn.get());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
::close(read_fd);
}
BENCHMARK(ProcessBatch_SingleSensor);
// --- process_batch_: 5 different sensors ---
// Measures batch processing with multiple items queued.
// This exercises the multi-message path in process_batch_.
static void ProcessBatch_5Sensors(benchmark::State &state) {
auto [conn, read_fd] = create_api_connection();
TestSensor sensors[5];
for (int i = 0; i < 5; i++) {
char name[20];
snprintf(name, sizeof(name), "sensor_%d", i);
sensors[i].configure(name);
sensors[i].publish_state(23.5f + static_cast<float>(i));
}
// Warm up batch vector
for (auto &s : sensors)
conn->send_sensor_state(&s);
bench_process_batch(conn.get());
drain_socket(read_fd);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
for (auto &s : sensors)
conn->send_sensor_state(&s);
bench_process_batch(conn.get());
}
drain_socket(read_fd);
benchmark::DoNotOptimize(conn.get());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
::close(read_fd);
}
BENCHMARK(ProcessBatch_5Sensors);
} // namespace esphome::api::benchmarks
#endif // USE_API_PLAINTEXT && USE_SENSOR
@@ -0,0 +1,115 @@
# Components needed for API protobuf benchmarks.
# Merged into the base config before validation so all
# dependencies get proper defaults.
#
# esphome: sub-keys are merged into the base config.
esphome:
areas:
- id: area_1
name: "Area 1"
- id: area_2
name: "Area 2"
- id: area_3
name: "Area 3"
- id: area_4
name: "Area 4"
- id: area_5
name: "Area 5"
- id: area_6
name: "Area 6"
- id: area_7
name: "Area 7"
- id: area_8
name: "Area 8"
- id: area_9
name: "Area 9"
- id: area_10
name: "Area 10"
- id: area_11
name: "Area 11"
- id: area_12
name: "Area 12"
- id: area_13
name: "Area 13"
- id: area_14
name: "Area 14"
- id: area_15
name: "Area 15"
- id: area_16
name: "Area 16"
- id: area_17
name: "Area 17"
- id: area_18
name: "Area 18"
- id: area_19
name: "Area 19"
- id: area_20
name: "Area 20"
devices:
- id: device_1
name: "Device 1"
area_id: area_1
- id: device_2
name: "Device 2"
area_id: area_2
- id: device_3
name: "Device 3"
area_id: area_3
- id: device_4
name: "Device 4"
area_id: area_4
- id: device_5
name: "Device 5"
area_id: area_5
- id: device_6
name: "Device 6"
area_id: area_6
- id: device_7
name: "Device 7"
area_id: area_7
- id: device_8
name: "Device 8"
area_id: area_8
- id: device_9
name: "Device 9"
area_id: area_9
- id: device_10
name: "Device 10"
area_id: area_10
- id: device_11
name: "Device 11"
area_id: area_11
- id: device_12
name: "Device 12"
area_id: area_12
- id: device_13
name: "Device 13"
area_id: area_13
- id: device_14
name: "Device 14"
area_id: area_14
- id: device_15
name: "Device 15"
area_id: area_15
- id: device_16
name: "Device 16"
area_id: area_16
- id: device_17
name: "Device 17"
area_id: area_17
- id: device_18
name: "Device 18"
area_id: area_18
- id: device_19
name: "Device 19"
area_id: area_19
- id: device_20
name: "Device 20"
area_id: area_20
api:
encryption:
sensor:
binary_sensor:
light:
switch:
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,61 @@
#include <benchmark/benchmark.h>
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome::binary_sensor::benchmarks {
static constexpr int kInnerIterations = 2000;
// Benchmark: publish_state with alternating values (forces state change every time)
static void BinarySensorPublish_Alternating(benchmark::State &state) {
BinarySensor sensor;
// First publish to establish initial state
sensor.publish_initial_state(false);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(i % 2 == 0);
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(BinarySensorPublish_Alternating);
// Benchmark: publish_state with same value (tests dedup fast path)
static void BinarySensorPublish_NoChange(benchmark::State &state) {
BinarySensor sensor;
sensor.publish_initial_state(true);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(true);
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(BinarySensorPublish_NoChange);
// Benchmark: publish_state with a callback registered
static void BinarySensorPublish_WithCallback(benchmark::State &state) {
BinarySensor sensor;
int callback_count = 0;
sensor.add_on_state_callback([&callback_count](bool) { callback_count++; });
sensor.publish_initial_state(false);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(i % 2 == 0);
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(BinarySensorPublish_WithCallback);
} // namespace esphome::binary_sensor::benchmarks
@@ -0,0 +1 @@
binary_sensor:
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,55 @@
#include <benchmark/benchmark.h>
#include "esphome/components/button/button.h"
namespace esphome::button::benchmarks {
static constexpr int kInnerIterations = 2000;
// Minimal Button for benchmarking — press_action() is a no-op.
class BenchButton : public Button {
public:
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
protected:
void press_action() override {}
};
// --- Button::press() ---
// Measures: ESP_LOGD + press_action() + callback dispatch.
static void ButtonPress(benchmark::State &state) {
BenchButton button;
button.configure("test_button");
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
button.press();
}
benchmark::DoNotOptimize(&button);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ButtonPress);
// --- Button::press() with callback ---
// Measures callback dispatch overhead.
static void ButtonPress_WithCallback(benchmark::State &state) {
BenchButton button;
button.configure("test_button");
uint64_t callback_count = 0;
button.add_on_press_callback([&callback_count]() { callback_count++; });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
button.press();
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ButtonPress_WithCallback);
} // namespace esphome::button::benchmarks
@@ -0,0 +1 @@
button:
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,142 @@
#include <benchmark/benchmark.h>
#include "esphome/components/climate/climate.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
// Minimal Climate for benchmarking — control() is a no-op.
class BenchClimate : public climate::Climate {
public:
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
climate::ClimateTraits traits() override { return this->traits_; }
climate::ClimateTraits traits_;
protected:
void control(const climate::ClimateCall & /*call*/) override {}
};
// Helper to create a typical HVAC climate device for benchmarks.
// Note: setup() is not called (no preferences backend), so save_state_()
// is effectively a no-op. This benchmarks the call/validation path, not persistence.
static void setup_hvac_climate(BenchClimate &climate) {
climate.configure("test_climate");
climate.traits_.set_supported_modes({
climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_HEAT_COOL,
climate::CLIMATE_MODE_COOL,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_FAN_ONLY,
});
climate.traits_.set_supported_fan_modes({
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH,
});
climate.traits_.set_supported_swing_modes({
climate::CLIMATE_SWING_OFF,
climate::CLIMATE_SWING_BOTH,
climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL,
});
climate.traits_.set_supported_presets({
climate::CLIMATE_PRESET_NONE,
climate::CLIMATE_PRESET_HOME,
climate::CLIMATE_PRESET_AWAY,
});
climate.traits_.set_visual_min_temperature(16.0f);
climate.traits_.set_visual_max_temperature(30.0f);
climate.traits_.set_visual_target_temperature_step(0.5f);
climate.traits_.set_visual_current_temperature_step(0.1f);
climate.traits_.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
}
// --- Climate::publish_state() with temperature update ---
// Measures the publish path for a thermostat reporting state —
// the hot path during HVAC operation.
static void ClimatePublish_State(benchmark::State &state) {
BenchClimate climate;
setup_hvac_climate(climate);
climate.mode = climate::CLIMATE_MODE_HEAT;
climate.action = climate::CLIMATE_ACTION_HEATING;
climate.target_temperature = 22.0f;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
climate.current_temperature = 20.0f + static_cast<float>(i % 100) / 10.0f;
climate.publish_state();
}
benchmark::DoNotOptimize(climate.current_temperature);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ClimatePublish_State);
// --- Climate::publish_state() with callback ---
// Measures callback dispatch overhead.
static void ClimatePublish_WithCallback(benchmark::State &state) {
BenchClimate climate;
setup_hvac_climate(climate);
climate.mode = climate::CLIMATE_MODE_HEAT;
climate.target_temperature = 22.0f;
uint64_t callback_count = 0;
climate.add_on_state_callback([&callback_count](climate::Climate & /*c*/) { callback_count++; });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
climate.current_temperature = 20.0f + static_cast<float>(i % 100) / 10.0f;
climate.publish_state();
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ClimatePublish_WithCallback);
// --- ClimateCall::perform() set target temperature ---
// The most common climate call — adjusting the thermostat setpoint.
static void ClimateCall_SetTemperature(benchmark::State &state) {
BenchClimate climate;
setup_hvac_climate(climate);
climate.mode = climate::CLIMATE_MODE_HEAT;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
float temp = 18.0f + static_cast<float>(i % 25) * 0.5f;
climate.make_call().set_target_temperature(temp).perform();
}
benchmark::DoNotOptimize(climate.target_temperature);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ClimateCall_SetTemperature);
// --- ClimateCall::perform() mode change with fan ---
// Exercises the validation path with multiple fields set.
static void ClimateCall_ModeChange(benchmark::State &state) {
BenchClimate climate;
setup_hvac_climate(climate);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
auto mode = (i % 2 == 0) ? climate::CLIMATE_MODE_HEAT : climate::CLIMATE_MODE_COOL;
auto fan = (i % 2 == 0) ? climate::CLIMATE_FAN_HIGH : climate::CLIMATE_FAN_LOW;
climate.make_call().set_mode(mode).set_fan_mode(fan).set_target_temperature(22.0f).perform();
}
benchmark::DoNotOptimize(climate.mode);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ClimateCall_ModeChange);
} // namespace esphome::benchmarks
@@ -0,0 +1 @@
climate:
@@ -0,0 +1,7 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# core (esphome/core/config.py) must run its to_code during builds
# because it bootstraps the fundamental application infrastructure.
manifest.enable_codegen()
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,107 @@
#include <benchmark/benchmark.h>
#include "esphome/components/cover/cover.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
// Minimal Cover for benchmarking — control() is a no-op.
class BenchCover : public cover::Cover {
public:
cover::CoverTraits get_traits() override { return this->traits_; }
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
cover::CoverTraits traits_;
protected:
void control(const cover::CoverCall & /*call*/) override {}
};
// --- Cover::publish_state() with position updates ---
// Measures the publish path for a garage door reporting position
// during open/close — the hot path during movement.
static void CoverPublish_Position(benchmark::State &state) {
BenchCover cover;
cover.configure("test_cover");
cover.traits_.set_supports_position(true);
cover.traits_.set_supports_tilt(false);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
cover.position = static_cast<float>(i % 101) / 100.0f;
cover.current_operation = (i % 2 == 0) ? cover::COVER_OPERATION_OPENING : cover::COVER_OPERATION_CLOSING;
cover.publish_state(false);
}
benchmark::DoNotOptimize(cover.position);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CoverPublish_Position);
// --- Cover::publish_state() with callback ---
// Measures callback dispatch overhead.
static void CoverPublish_WithCallback(benchmark::State &state) {
BenchCover cover;
cover.configure("test_cover");
cover.traits_.set_supports_position(true);
uint64_t callback_count = 0;
cover.add_on_state_callback([&callback_count]() { callback_count++; });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
cover.position = static_cast<float>(i % 101) / 100.0f;
cover.publish_state(false);
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CoverPublish_WithCallback);
// --- CoverCall::perform() open/close cycle ---
// Measures the full call path: validation + control delegation.
static void CoverCall_OpenClose(benchmark::State &state) {
BenchCover cover;
cover.configure("test_cover");
cover.traits_.set_supports_position(true);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
if (i % 2 == 0) {
cover.make_call().set_command_open().perform();
} else {
cover.make_call().set_command_close().perform();
}
}
benchmark::DoNotOptimize(cover.position);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CoverCall_OpenClose);
// --- CoverCall::perform() set position ---
// Measures the position-setting call path.
static void CoverCall_SetPosition(benchmark::State &state) {
BenchCover cover;
cover.configure("test_cover");
cover.traits_.set_supports_position(true);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
float pos = static_cast<float>(i % 101) / 100.0f;
cover.make_call().set_position(pos).perform();
}
benchmark::DoNotOptimize(cover.position);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CoverCall_SetPosition);
} // namespace esphome::benchmarks
@@ -0,0 +1 @@
cover:
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,122 @@
#include <benchmark/benchmark.h>
#include "esphome/components/fan/fan.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
// Minimal Fan for benchmarking — control() is a no-op.
class BenchFan : public fan::Fan {
public:
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
fan::FanTraits get_traits() override { return this->traits_; }
fan::FanTraits traits_;
protected:
void control(const fan::FanCall & /*call*/) override {}
};
// Helper to create a typical fan device for benchmarks.
// Note: setup() is not called (no preferences backend), so save_state_()
// is effectively a no-op. This benchmarks the call/validation path, not persistence.
static void setup_fan(BenchFan &fan) {
fan.configure("test_fan");
fan.traits_.set_oscillation(true);
fan.traits_.set_speed(true);
fan.traits_.set_supported_speed_count(6);
fan.traits_.set_direction(true);
fan.set_restore_mode(fan::FanRestoreMode::NO_RESTORE);
fan.traits_.set_supported_preset_modes({
"auto",
"sleep",
"nature",
"turbo",
});
}
// --- Fan::publish_state() with speed update ---
// Measures the publish path for a fan reporting state —
// the hot path during fan operation.
static void FanPublish_State(benchmark::State &state) {
BenchFan fan;
setup_fan(fan);
fan.state = true;
fan.direction = fan::FanDirection::FORWARD;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
fan.speed = (i % 6) + 1;
fan.publish_state();
}
benchmark::DoNotOptimize(fan.speed);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FanPublish_State);
// --- Fan::publish_state() with callback ---
// Measures callback dispatch overhead.
static void FanPublish_WithCallback(benchmark::State &state) {
BenchFan fan;
setup_fan(fan);
fan.state = true;
uint64_t callback_count = 0;
fan.add_on_state_callback([&callback_count]() { callback_count++; });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
fan.speed = (i % 6) + 1;
fan.publish_state();
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FanPublish_WithCallback);
// --- FanCall::perform() set speed ---
// The most common fan call — adjusting the speed level.
static void FanCall_SetSpeed(benchmark::State &state) {
BenchFan fan;
setup_fan(fan);
fan.state = true;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
int speed = (i % 6) + 1;
fan.make_call().set_speed(speed).perform();
}
benchmark::DoNotOptimize(fan.speed);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FanCall_SetSpeed);
// --- FanCall::perform() with multiple fields ---
// Exercises the validation path with state, speed, oscillation, and direction.
static void FanCall_MultiField(benchmark::State &state) {
BenchFan fan;
setup_fan(fan);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
auto dir = (i % 2 == 0) ? fan::FanDirection::FORWARD : fan::FanDirection::REVERSE;
int speed = (i % 6) + 1;
fan.make_call().set_state(true).set_speed(speed).set_oscillating(i % 2 == 0).set_direction(dir).perform();
}
benchmark::DoNotOptimize(fan.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FanCall_MultiField);
} // namespace esphome::benchmarks
@@ -0,0 +1 @@
fan:
@@ -0,0 +1,7 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# host must run its to_code during builds because it sets up
# the host platform target execution environment.
manifest.enable_codegen()
@@ -0,0 +1,7 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# json must run its to_code during benchmark builds because it
# adds the ArduinoJson library dependency needed by the API component.
manifest.enable_codegen()
@@ -0,0 +1,28 @@
import esphome.codegen as cg
from esphome.components.light import generate_gamma_table
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# Light benchmarks need USE_LIGHT_GAMMA_LUT defined and a gamma table
# with external linkage that the benchmark .cpp can reference.
manifest.enable_codegen()
original_to_code = manifest.to_code
async def to_code(config):
await original_to_code(config)
cg.add_define("USE_LIGHT_GAMMA_LUT")
# Use the light component's own generate_gamma_table() so the
# benchmark stays in sync with any formula changes.
forward = generate_gamma_table(2.8)
values = ", ".join(f"0x{int(v):04X}" for v in forward)
# Use extern-visible (non-static) array so the benchmark .cpp
# can reference it via extern declaration.
cg.add_global(
cg.RawStatement(
f"extern const uint16_t bench_gamma_2_8_fwd[256] PROGMEM = {{{values}}};"
)
)
to_code.priority = original_to_code.priority
manifest.to_code = to_code
@@ -0,0 +1,253 @@
#include <benchmark/benchmark.h>
#include "esphome/components/light/light_output.h"
#include "esphome/components/light/light_state.h"
// Gamma 2.8 forward LUT generated by the light component's Python codegen
// (see tests/benchmarks/components/light/__init__.py which calls generate_gamma_table())
extern const uint16_t bench_gamma_2_8_fwd[256];
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
// Minimal LightOutput for benchmarking — no real hardware interaction.
class BenchLightOutput : public light::LightOutput {
public:
light::LightTraits get_traits() override { return this->traits_; }
void write_state(light::LightState * /*state*/) override {}
light::LightTraits traits_;
};
// Test subclass to access protected configure_entity_() for benchmark setup.
class TestLightState : public light::LightState {
public:
using LightState::LightState;
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
};
// Helper to create a configured RGBWW light state for benchmarks.
// Note: setup() is not called (no preferences backend), so save_remote_values_()
// is effectively a no-op. This benchmarks the call/validation path, not persistence.
static void setup_rgbww_light(BenchLightOutput &output, TestLightState &light) {
output.traits_.set_supported_color_modes({light::ColorMode::RGB_COLD_WARM_WHITE});
output.traits_.set_min_mireds(153.0f);
output.traits_.set_max_mireds(500.0f);
light.configure("test_light");
light.set_default_transition_length(0);
light.set_gamma_correct(2.8f);
light.set_gamma_table(bench_gamma_2_8_fwd);
light.set_restore_mode(light::LIGHT_ALWAYS_OFF);
}
// --- LightCall::perform() with instant RGB color change (Home Assistant API path) ---
// Measures the full call path: validation, set_immediately_, publish, and save.
// HA sends color_mode explicitly since API 1.6.
static void LightCall_RGBInstant(benchmark::State &state) {
BenchLightOutput output;
TestLightState light(&output);
setup_rgbww_light(output, light);
// Turn on first so subsequent calls are color changes
light.make_call().set_state(true).set_brightness(1.0f).set_color_brightness(1.0f).set_transition_length(0).perform();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
float v = static_cast<float>(i % 256) / 255.0f;
light.make_call()
.set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE)
.set_red(v)
.set_green(1.0f - v)
.set_blue(v * 0.5f)
.set_transition_length(0)
.perform();
}
benchmark::DoNotOptimize(light.remote_values);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(LightCall_RGBInstant);
// --- LightCall::perform() turn on/off cycle (Home Assistant API path) ---
// HA sends color_mode explicitly since API 1.6, skipping compute_color_mode_().
static void LightCall_ToggleOnOff(benchmark::State &state) {
BenchLightOutput output;
TestLightState light(&output);
setup_rgbww_light(output, light);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
light.make_call()
.set_state(i % 2 == 0)
.set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE)
.set_transition_length(0)
.perform();
}
benchmark::DoNotOptimize(light.remote_values);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(LightCall_ToggleOnOff);
// --- LightCall::perform() turn on/off via MQTT ---
// MQTT never sends color_mode, so compute_color_mode_() runs every call.
static void LightCall_ToggleOnOff_MQTT(benchmark::State &state) {
BenchLightOutput output;
TestLightState light(&output);
setup_rgbww_light(output, light);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
light.make_call().set_state(i % 2 == 0).set_transition_length(0).perform();
}
benchmark::DoNotOptimize(light.remote_values);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(LightCall_ToggleOnOff_MQTT);
// --- LightCall::perform() with color temperature via MQTT ---
// Exercises the transform_parameters_() path that converts color_temperature
// to cold/warm white fractions. MQTT never sends color_mode, so this also
// hits compute_color_mode_() every call. Modern HA avoids this path entirely
// by converting color temp to CW/WW client-side.
static void LightCall_ColorTemperature_MQTT(benchmark::State &state) {
BenchLightOutput output;
TestLightState light(&output);
setup_rgbww_light(output, light);
light.make_call().set_state(true).set_brightness(1.0f).set_transition_length(0).perform();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
// Sweep through color temperature range
float ct = 153.0f + static_cast<float>(i % 348);
light.make_call().set_color_temperature(ct).set_transition_length(0).perform();
}
benchmark::DoNotOptimize(light.remote_values);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(LightCall_ColorTemperature_MQTT);
// --- LightCall::perform() with 1s transition (Home Assistant API path) ---
// Exercises start_transition_() which allocates a LightTransformer.
// This is the default HA path when transition_length > 0.
static void LightCall_Transition(benchmark::State &state) {
BenchLightOutput output;
TestLightState light(&output);
setup_rgbww_light(output, light);
light.make_call().set_state(true).set_brightness(1.0f).set_transition_length(0).perform();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
float v = static_cast<float>(i % 256) / 255.0f;
light.make_call()
.set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE)
.set_red(v)
.set_green(1.0f - v)
.set_blue(v * 0.5f)
.set_transition_length(1000)
.perform();
}
benchmark::DoNotOptimize(light.remote_values);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(LightCall_Transition);
// --- LightCall::perform() with cold/warm white (Home Assistant API path) ---
// Mirrors what modern HA sends: explicit color_mode with direct cold_white
// and warm_white values. HA converts color temp to CW/WW client-side for
// CWWW lights (API >= 1.6), so this is the primary HA path.
static void LightCall_ColdWarmWhite(benchmark::State &state) {
BenchLightOutput output;
TestLightState light(&output);
setup_rgbww_light(output, light);
light.make_call().set_state(true).set_brightness(1.0f).set_transition_length(0).perform();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
float frac = static_cast<float>(i % 256) / 255.0f;
light.make_call()
.set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE)
.set_cold_white(1.0f - frac)
.set_warm_white(frac)
.set_transition_length(0)
.perform();
}
benchmark::DoNotOptimize(light.remote_values);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(LightCall_ColdWarmWhite);
// --- LightState::publish_state() with a remote values listener ---
// Measures listener notification overhead.
static void LightPublish_WithListener(benchmark::State &state) {
BenchLightOutput output;
TestLightState light(&output);
setup_rgbww_light(output, light);
struct TestListener : public light::LightRemoteValuesListener {
void on_light_remote_values_update() override { count_++; }
uint64_t count_{0};
} listener;
light.add_remote_values_listener(&listener);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
light.publish_state();
}
benchmark::DoNotOptimize(listener.count_);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(LightPublish_WithListener);
// --- current_values_as_rgbww output conversion with gamma LUT ---
// Measures the output conversion path that real light drivers call
// from write_state() to get hardware PWM values, including gamma
// table lookups via the LUT generated by Python codegen.
static void LightOutput_RGBWW(benchmark::State &state) {
BenchLightOutput output;
TestLightState light(&output);
setup_rgbww_light(output, light);
light.make_call()
.set_state(true)
.set_brightness(0.8f)
.set_color_brightness(0.6f)
.set_red(1.0f)
.set_green(0.5f)
.set_blue(0.2f)
.set_cold_white(0.7f)
.set_warm_white(0.3f)
.set_transition_length(0)
.perform();
float r, g, b, cw, ww;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
light.current_values_as_rgbww(&r, &g, &b, &cw, &ww);
}
benchmark::DoNotOptimize(r);
benchmark::DoNotOptimize(cw);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(LightOutput_RGBWW);
} // namespace esphome::benchmarks
@@ -0,0 +1 @@
light:
@@ -0,0 +1,7 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# logger must run its to_code during builds because it configures
# the logging subsystem used by ESP_LOG* macros.
manifest.enable_codegen()
+42
View File
@@ -0,0 +1,42 @@
#include <benchmark/benchmark.h>
#include "esphome/components/logger/logger.h"
/*
This special main.cpp provides the entry point for Google Benchmark.
It replaces the default ESPHome main with a benchmark runner.
*/
// Auto generated code by esphome
// ========== AUTO GENERATED INCLUDE BLOCK BEGIN ===========
// ========== AUTO GENERATED INCLUDE BLOCK END ===========
void original_setup() {
// Code-generated App initialization (pre_setup, area/device registration, etc.)
// ========== AUTO GENERATED CODE BEGIN ===========
// =========== AUTO GENERATED CODE END ============
}
void setup() {
// Run auto-generated initialization (App.pre_setup, area/device registration,
// looping_components_.init, etc.) so benchmarks that use App work correctly.
original_setup();
// Log functions call global_logger->log_vprintf_() without a null check,
// so we must set up a Logger before any test that triggers logging.
static esphome::logger::Logger test_logger(0);
test_logger.set_log_level(ESPHOME_LOG_LEVEL);
test_logger.pre_setup();
int argc = 1;
char arg0[] = "benchmark";
char *argv[] = {arg0, nullptr};
::benchmark::Initialize(&argc, argv);
::benchmark::RunSpecifiedBenchmarks();
::benchmark::Shutdown();
exit(0);
}
void loop() {}
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,121 @@
#include <benchmark/benchmark.h>
#include "esphome/components/number/number.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
// Minimal Number for benchmarking — control() publishes the value back.
class BenchNumber : public number::Number {
public:
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
protected:
void control(float value) override { this->publish_state(value); }
};
// Helper to create a typical number entity for benchmarks.
static void setup_number(BenchNumber &number) {
number.configure("test_number");
number.traits.set_min_value(0.0f);
number.traits.set_max_value(100.0f);
number.traits.set_step(1.0f);
number.traits.set_mode(number::NUMBER_MODE_SLIDER);
}
// --- Number::publish_state() ---
// Measures the publish path: set_has_state, store value, callback dispatch.
static void NumberPublish_State(benchmark::State &state) {
BenchNumber number;
setup_number(number);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
number.publish_state(static_cast<float>(i % 100));
}
benchmark::DoNotOptimize(number.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(NumberPublish_State);
// --- Number::publish_state() with callback ---
// Measures callback dispatch overhead.
static void NumberPublish_WithCallback(benchmark::State &state) {
BenchNumber number;
setup_number(number);
uint64_t callback_count = 0;
number.add_on_state_callback([&callback_count](float) { callback_count++; });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
number.publish_state(static_cast<float>(i % 100));
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(NumberPublish_WithCallback);
// --- NumberCall::perform() set value ---
// The most common number call — setting an absolute value.
// Exercises: validation against min/max, control() dispatch.
static void NumberCall_SetValue(benchmark::State &state) {
BenchNumber number;
setup_number(number);
number.publish_state(50.0f);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
float val = static_cast<float>(i % 100);
number.make_call().set_value(val).perform();
}
benchmark::DoNotOptimize(number.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(NumberCall_SetValue);
// --- NumberCall::perform() increment ---
// Exercises: state read, step arithmetic, max clamping.
static void NumberCall_Increment(benchmark::State &state) {
BenchNumber number;
setup_number(number);
number.publish_state(0.0f);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
number.make_call().number_increment(true).perform();
}
benchmark::DoNotOptimize(number.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(NumberCall_Increment);
// --- NumberCall::perform() decrement ---
// Exercises: state read, step arithmetic, min clamping.
static void NumberCall_Decrement(benchmark::State &state) {
BenchNumber number;
setup_number(number);
number.publish_state(100.0f);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
number.make_call().number_decrement(true).perform();
}
benchmark::DoNotOptimize(number.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(NumberCall_Decrement);
} // namespace esphome::benchmarks
@@ -0,0 +1 @@
number:
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,157 @@
#include <benchmark/benchmark.h>
#include "esphome/components/select/select.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
// Minimal Select for benchmarking — control() publishes directly by index.
class BenchSelect : public select::Select {
public:
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
protected:
void control(size_t index) override { this->publish_state(index); }
};
// Helper to create a select with the given options.
static void setup_select(BenchSelect &select, const char *name, std::initializer_list<const char *> options) {
select.configure(name);
select.traits.set_options(options);
select.publish_state(size_t(0));
}
// --- Select::publish_state(size_t) ---
// The fast path: publish by index, no string lookup.
static void SelectPublish_ByIndex(benchmark::State &state) {
BenchSelect select;
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.publish_state(static_cast<size_t>(i % 4));
}
benchmark::DoNotOptimize(select.active_index());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectPublish_ByIndex);
// --- Select::publish_state(const char *) ---
// The string path: requires index_of() lookup via strncmp.
static void SelectPublish_ByString(benchmark::State &state) {
BenchSelect select;
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
const char *options[] = {"off", "still", "move", "still+move"};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.publish_state(options[i % 4]);
}
benchmark::DoNotOptimize(select.active_index());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectPublish_ByString);
// --- Select::publish_state() with callback ---
// Measures callback dispatch overhead on the index path.
static void SelectPublish_WithCallback(benchmark::State &state) {
BenchSelect select;
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
uint64_t callback_count = 0;
select.add_on_state_callback([&callback_count](size_t) { callback_count++; });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.publish_state(static_cast<size_t>(i % 4));
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectPublish_WithCallback);
// --- SelectCall::perform() set by index ---
// The fast call path — no string matching needed.
static void SelectCall_SetByIndex(benchmark::State &state) {
BenchSelect select;
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.make_call().set_index(i % 4).perform();
}
benchmark::DoNotOptimize(select.active_index());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectCall_SetByIndex);
// --- SelectCall::perform() set by option string ---
// Exercises the string lookup path through index_of().
static void SelectCall_SetByOption(benchmark::State &state) {
BenchSelect select;
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
const char *options[] = {"off", "still", "move", "still+move"};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.make_call().set_option(options[i % 4]).perform();
}
benchmark::DoNotOptimize(select.active_index());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectCall_SetByOption);
// --- SelectCall::perform() next with cycling ---
// Exercises the navigation path through active_index_.
static void SelectCall_NextCycle(benchmark::State &state) {
BenchSelect select;
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.make_call().select_next(true).perform();
}
benchmark::DoNotOptimize(select.active_index());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectCall_NextCycle);
// --- SelectCall with 10 options (string lookup) ---
// Worst-case string matching with more options.
static void SelectCall_SetByOption_10Options(benchmark::State &state) {
BenchSelect select;
setup_select(
select, "test_select",
{"off", "still", "move", "still+move", "custom1", "custom2", "custom3", "custom4", "custom5", "custom6"});
// Pick options spread across the list to exercise different search depths
const char *picks[] = {"off", "custom3", "custom6", "move"};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.make_call().set_option(picks[i % 4]).perform();
}
benchmark::DoNotOptimize(select.active_index());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectCall_SetByOption_10Options);
} // namespace esphome::benchmarks
@@ -0,0 +1 @@
select:
@@ -0,0 +1,12 @@
import esphome.codegen as cg
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# Sensor filter benchmarks need USE_SENSOR_FILTER defined.
# We use a custom to_code instead of enable_codegen() to avoid
# pulling in the full sensor component setup.
async def to_code(config):
cg.add_define("USE_SENSOR_FILTER")
manifest.to_code = to_code
@@ -0,0 +1,78 @@
#include <benchmark/benchmark.h>
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensor/filter.h"
namespace esphome::sensor::benchmarks {
static constexpr int kInnerIterations = 2000;
// Benchmark: sensor publish through a SlidingWindowMovingAverageFilter (window=5, send_every=1)
static void SensorFilter_SlidingWindowAvg(benchmark::State &state) {
Sensor sensor;
// Create filter: window_size=5, send_every=1, send_first_at=1
auto *filter = new SlidingWindowMovingAverageFilter(5, 1, 1);
sensor.add_filter(filter);
float value = 0.0f;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(value);
value += 0.1f;
if (value > 1000.0f)
value = 0.0f;
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SensorFilter_SlidingWindowAvg);
// Benchmark: sensor publish through ExponentialMovingAverageFilter
static void SensorFilter_ExponentialMovingAvg(benchmark::State &state) {
Sensor sensor;
// alpha=0.1, send_every=1, send_first_at=1
auto *filter = new ExponentialMovingAverageFilter(0.1f, 1, 1);
sensor.add_filter(filter);
float value = 0.0f;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(value);
value += 0.1f;
if (value > 1000.0f)
value = 0.0f;
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SensorFilter_ExponentialMovingAvg);
// Benchmark: sensor publish through a chain of 3 filters (offset + multiply + sliding window)
static void SensorFilter_Chain3(benchmark::State &state) {
Sensor sensor;
sensor.add_filters({
new OffsetFilter([]() -> float { return 1.0f; }),
new MultiplyFilter([]() -> float { return 2.0f; }),
new SlidingWindowMovingAverageFilter(5, 1, 1),
});
float value = 0.0f;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(value);
value += 0.1f;
if (value > 1000.0f)
value = 0.0f;
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SensorFilter_Chain3);
} // namespace esphome::sensor::benchmarks
@@ -0,0 +1,79 @@
#include <benchmark/benchmark.h>
#include "esphome/components/sensor/sensor.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
// sub-microsecond benchmarks.
static constexpr int kInnerIterations = 2000;
// Test subclass to access protected configure_entity_() for benchmark setup.
class TestSensor : public sensor::Sensor {
public:
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
};
// --- Sensor::publish_state() with no callbacks registered ---
// Measures baseline publish overhead: state assignment, logging,
// internal_send_state_to_frontend, ControllerRegistry notification.
static void SensorPublish_NoCallbacks(benchmark::State &state) {
TestSensor sensor;
sensor.configure("test_sensor");
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(static_cast<float>(i));
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SensorPublish_NoCallbacks);
// --- Sensor::publish_state() with one state callback ---
// Measures callback dispatch overhead through LazyCallbackManager.
static void SensorPublish_WithCallback(benchmark::State &state) {
TestSensor sensor;
sensor.configure("test_sensor");
float callback_value = 0.0f;
sensor.add_on_state_callback([&callback_value](float value) { callback_value = value; });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(static_cast<float>(i));
}
benchmark::DoNotOptimize(callback_value);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SensorPublish_WithCallback);
// --- Sensor::publish_state() with the same value every time ---
// Steady-state pattern: sensor reports an unchanged reading.
// Sensor doesn't dedup today, so this exercises the same code path
// as changing values, but tracks the common real-world pattern
// separately for regression detection.
static void SensorPublish_SameValue(benchmark::State &state) {
TestSensor sensor;
sensor.configure("test_sensor");
// Warm up so has_state is already set
sensor.publish_state(23.5f);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(23.5f);
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SensorPublish_SameValue);
} // namespace esphome::benchmarks
@@ -0,0 +1 @@
sensor:
@@ -0,0 +1,7 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# socket must run its to_code to define USE_SOCKET_IMPL_BSD_SOCKETS
# which is needed by the api frame helper benchmarks.
manifest.enable_codegen()
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,137 @@
#include <benchmark/benchmark.h>
#include "esphome/components/switch/switch.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
// Minimal Switch for benchmarking — write_state() publishes directly.
class BenchSwitch : public switch_::Switch {
public:
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
protected:
void write_state(bool state) override { this->publish_state(state); }
};
// --- Switch::publish_state() alternating ---
// Forces state change every call, exercising the full publish path.
static void SwitchPublish_Alternating(benchmark::State &state) {
BenchSwitch sw;
sw.configure("test_switch");
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
sw.publish_state(false);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sw.publish_state(i % 2 == 0);
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SwitchPublish_Alternating);
// --- Switch::publish_state() no change ---
// Tests the deduplication fast path in publish_dedup_.
static void SwitchPublish_NoChange(benchmark::State &state) {
BenchSwitch sw;
sw.configure("test_switch");
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
sw.publish_state(true);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sw.publish_state(true);
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SwitchPublish_NoChange);
// --- Switch::publish_state() with callback ---
// Measures callback dispatch overhead on state changes.
static void SwitchPublish_WithCallback(benchmark::State &state) {
BenchSwitch sw;
sw.configure("test_switch");
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
uint64_t callback_count = 0;
sw.add_on_state_callback([&callback_count](bool) { callback_count++; });
sw.publish_state(false);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sw.publish_state(i % 2 == 0);
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SwitchPublish_WithCallback);
// --- Switch::turn_on() / turn_off() ---
// The front-end call path: turn_on → write_state → publish_state.
static void SwitchTurnOn(benchmark::State &state) {
BenchSwitch sw;
sw.configure("test_switch");
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
sw.publish_state(false);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sw.turn_on();
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SwitchTurnOn);
// --- Switch::toggle() alternating ---
// Exercises the toggle path which reads current state to determine target.
static void SwitchToggle(benchmark::State &state) {
BenchSwitch sw;
sw.configure("test_switch");
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
sw.publish_state(false);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sw.toggle();
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SwitchToggle);
// --- Switch::publish_state() inverted ---
// Verifies the inversion path doesn't add significant overhead.
static void SwitchPublish_Inverted(benchmark::State &state) {
BenchSwitch sw;
sw.configure("test_switch");
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
sw.set_inverted(true);
sw.publish_state(false);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sw.publish_state(i % 2 == 0);
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SwitchPublish_Inverted);
} // namespace esphome::benchmarks
@@ -0,0 +1 @@
switch:
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,108 @@
#include <benchmark/benchmark.h>
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome::text_sensor::benchmarks {
static constexpr int kInnerIterations = 2000;
// --- publish_state(const char *) with short string, value changes each time ---
// Exercises: memcmp check (mismatch), string assign, callback dispatch.
static void TextSensorPublish_Short_Changing(benchmark::State &state) {
TextSensor sensor;
// Pre-populate with different short strings
const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(values[i % 4]);
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(TextSensorPublish_Short_Changing);
// --- publish_state(const char *) with short string, same value (dedup path) ---
// Exercises: memcmp check (match), skips string assign.
static void TextSensorPublish_Short_NoChange(benchmark::State &state) {
TextSensor sensor;
sensor.publish_state("192.168.1.100");
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state("192.168.1.100");
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(TextSensorPublish_Short_NoChange);
// --- publish_state with longer string (firmware version, MAC address) ---
// Exercises: memcmp on longer strings, string assign with potential realloc.
static void TextSensorPublish_Long_Changing(benchmark::State &state) {
TextSensor sensor;
const char *values[] = {
"2025.12.0-dev (Jan 15 2025, 10:30:00)",
"2025.12.1-dev (Feb 20 2025, 14:45:00)",
"2025.12.2-dev (Mar 10 2025, 08:15:00)",
"2025.12.3-dev (Apr 5 2025, 16:00:00)",
};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(values[i % 4]);
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(TextSensorPublish_Long_Changing);
// --- publish_state with callback ---
// Measures callback dispatch overhead for text sensors.
static void TextSensorPublish_WithCallback(benchmark::State &state) {
TextSensor sensor;
uint64_t callback_count = 0;
sensor.add_on_state_callback([&callback_count](const std::string &) { callback_count++; });
const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(values[i % 4]);
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(TextSensorPublish_WithCallback);
// --- publish_state(const char *, size_t) direct ---
// The lowest-level overload, avoids strlen.
static void TextSensorPublish_WithLen(benchmark::State &state) {
TextSensor sensor;
static constexpr const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"};
static constexpr size_t lens[] = {11, 11, 11, 11};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(values[i % 4], lens[i % 4]);
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(TextSensorPublish_WithLen);
} // namespace esphome::text_sensor::benchmarks
@@ -0,0 +1 @@
text_sensor:
@@ -0,0 +1,9 @@
import esphome.codegen as cg
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
async def to_code(config):
cg.add_build_flag("-DUSE_TIME_TIMEZONE")
manifest.to_code = to_code
@@ -0,0 +1,22 @@
#include <benchmark/benchmark.h>
#include "esphome/core/application.h"
namespace esphome::benchmarks {
// Benchmark Application::loop() with no registered components.
// App is initialized by original_setup() in main.cpp (code-generated
// pre_setup, area/device registration, looping_components_.init).
// This measures the baseline overhead of the main loop: scheduler,
// timing, before/after loop tasks, and yield_with_select_.
static void ApplicationLoop_Empty(benchmark::State &state) {
// Set loop interval to 0 so yield_with_select_ returns immediately
// instead of sleeping. This benchmarks the loop overhead, not the sleep.
App.set_loop_interval(0);
for (auto _ : state) {
App.loop();
}
}
BENCHMARK(ApplicationLoop_Empty);
} // namespace esphome::benchmarks
+366
View File
@@ -0,0 +1,366 @@
#include <benchmark/benchmark.h>
#include <cinttypes>
#include <cstdio>
#include "esphome/core/helpers.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
// sub-microsecond benchmarks.
static constexpr int kInnerIterations = 2000;
// --- random_float() ---
static void RandomFloat(benchmark::State &state) {
for (auto _ : state) {
float result = 0.0f;
for (int i = 0; i < kInnerIterations; i++) {
result += random_float();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(RandomFloat);
// --- random_uint32() ---
static void RandomUint32(benchmark::State &state) {
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result += random_uint32();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(RandomUint32);
// --- format_hex_to() - 6 bytes (MAC address sized) ---
static void FormatHexTo_6Bytes(benchmark::State &state) {
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45};
char buffer[13]; // 6 * 2 + 1
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_hex_to(buffer, data, 6);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatHexTo_6Bytes);
// --- format_hex_to() - 16 bytes (UUID sized) ---
static void FormatHexTo_16Bytes(benchmark::State &state) {
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89,
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10};
char buffer[33]; // 16 * 2 + 1
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_hex_to(buffer, data, 16);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatHexTo_16Bytes);
// --- format_hex_to() - 100 bytes (large payload) ---
static void FormatHexTo_100Bytes(benchmark::State &state) {
uint8_t data[100];
for (int i = 0; i < 100; i++) {
data[i] = static_cast<uint8_t>(i);
}
char buffer[201]; // 100 * 2 + 1
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_hex_to(buffer, data, 100);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatHexTo_100Bytes);
// --- format_hex_pretty_to() - 6 bytes with ':' separator ---
static void FormatHexPrettyTo_6Bytes(benchmark::State &state) {
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45};
char buffer[18]; // 6 * 3
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_hex_pretty_to(buffer, data, 6);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatHexPrettyTo_6Bytes);
// --- format_mac_addr_upper() ---
static void FormatMacAddrUpper(benchmark::State &state) {
const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
format_mac_addr_upper(mac, buffer);
}
benchmark::DoNotOptimize(buffer);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(FormatMacAddrUpper);
// --- fnv1_hash() - short string ---
static void Fnv1Hash_Short(benchmark::State &state) {
const char *str = "sensor.temperature";
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1_hash(str);
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1Hash_Short);
// --- fnv1_hash() - long string ---
static void Fnv1Hash_Long(benchmark::State &state) {
const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected";
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1_hash(str);
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1Hash_Long);
// --- fnv1a_hash() - short string ---
// Use DoNotOptimize on the input pointer to prevent constexpr evaluation
static void Fnv1aHash_Short(benchmark::State &state) {
const char *str = "sensor.temperature";
benchmark::DoNotOptimize(str);
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1a_hash(str);
benchmark::ClobberMemory();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1aHash_Short);
// --- fnv1a_hash() - long string ---
static void Fnv1aHash_Long(benchmark::State &state) {
const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected";
benchmark::DoNotOptimize(str);
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1a_hash(str);
benchmark::ClobberMemory();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1aHash_Long);
// --- fnv1_hash_object_id() - typical entity name ---
static void Fnv1HashObjectId(benchmark::State &state) {
char name[] = "Living Room Temperature Sensor";
size_t len = sizeof(name) - 1;
benchmark::DoNotOptimize(name);
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= fnv1_hash_object_id(name, len);
benchmark::ClobberMemory();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Fnv1HashObjectId);
// --- parse_hex() - 6 bytes from string ---
static void ParseHex_6Bytes(benchmark::State &state) {
const char *hex_str = "ABCDEF012345";
uint8_t data[6];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
parse_hex(hex_str, data, 6);
}
benchmark::DoNotOptimize(data);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ParseHex_6Bytes);
// --- parse_hex() - 16 bytes from string ---
static void ParseHex_16Bytes(benchmark::State &state) {
const char *hex_str = "ABCDEF0123456789FEDCBA9876543210";
uint8_t data[16];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
parse_hex(hex_str, data, 16);
}
benchmark::DoNotOptimize(data);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ParseHex_16Bytes);
// --- crc8() - 8 bytes ---
static void CRC8_8Bytes(benchmark::State &state) {
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
for (auto _ : state) {
uint8_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= crc8(data, 8);
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CRC8_8Bytes);
// --- crc16() - 8 bytes ---
static void CRC16_8Bytes(benchmark::State &state) {
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
for (auto _ : state) {
uint16_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result ^= crc16(data, 8);
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CRC16_8Bytes);
// --- value_accuracy_to_buf() - typical sensor value ---
static void ValueAccuracyToBuf(benchmark::State &state) {
char raw_buf[VALUE_ACCURACY_MAX_LEN] = {};
std::span<char, VALUE_ACCURACY_MAX_LEN> buf(raw_buf);
float value = 23.456f;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
value_accuracy_to_buf(buf, value, 2);
}
benchmark::DoNotOptimize(raw_buf);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ValueAccuracyToBuf);
// --- int8_to_str() ---
static void Int8ToStr(benchmark::State &state) {
char buffer[5] = {};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
int8_to_str(buffer, static_cast<int8_t>(i & 0xFF));
benchmark::DoNotOptimize(buffer);
benchmark::ClobberMemory();
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Int8ToStr);
// --- base64_decode() - into pre-allocated buffer ---
static void Base64Decode_32Bytes(benchmark::State &state) {
// 32 bytes encoded = 44 base64 chars
const uint8_t encoded[] = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGx0eHw==";
size_t encoded_len = 44;
uint8_t output[32];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
base64_decode(encoded, encoded_len, output, sizeof(output));
}
benchmark::DoNotOptimize(output);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Base64Decode_32Bytes);
// --- uint32_to_str() vs snprintf ---
static void Uint32ToStr_Small(benchmark::State &state) {
char buf[UINT32_MAX_STR_SIZE];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uint32_to_str(buf, 12345);
benchmark::DoNotOptimize(buf);
benchmark::ClobberMemory();
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Uint32ToStr_Small);
static void Snprintf_Uint32_Small(benchmark::State &state) {
char buf[UINT32_MAX_STR_SIZE];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
snprintf(buf, sizeof(buf), "%" PRIu32, static_cast<uint32_t>(12345));
benchmark::DoNotOptimize(buf);
benchmark::ClobberMemory();
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Snprintf_Uint32_Small);
static void Uint32ToStr_Large(benchmark::State &state) {
char buf[UINT32_MAX_STR_SIZE];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uint32_to_str(buf, 4294967295u);
benchmark::DoNotOptimize(buf);
benchmark::ClobberMemory();
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Uint32ToStr_Large);
static void Snprintf_Uint32_Large(benchmark::State &state) {
char buf[UINT32_MAX_STR_SIZE];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
snprintf(buf, sizeof(buf), "%" PRIu32, static_cast<uint32_t>(4294967295u));
benchmark::DoNotOptimize(buf);
benchmark::ClobberMemory();
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Snprintf_Uint32_Large);
} // namespace esphome::benchmarks
+54
View File
@@ -0,0 +1,54 @@
#include <benchmark/benchmark.h>
#include "esphome/core/log.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
// sub-microsecond benchmarks.
static constexpr int kInnerIterations = 2000;
static const char *const TAG = "bench";
// --- Log a message with no format specifiers (fastest path) ---
static void Logger_NoFormat(benchmark::State &state) {
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ESP_LOGW(TAG, "Something happened");
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Logger_NoFormat);
// --- Log a message with 3 uint32_t format specifiers ---
static void Logger_3Uint32(benchmark::State &state) {
uint32_t a = 12345, b = 67890, c = 99999;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ESP_LOGW(TAG, "Values: %" PRIu32 " %" PRIu32 " %" PRIu32, a, b, c);
}
benchmark::DoNotOptimize(a);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Logger_3Uint32);
// --- Log a message with 3 floats (common for sensor values) ---
static void Logger_3Float(benchmark::State &state) {
float temp = 23.456f, humidity = 67.89f, pressure = 1013.25f;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ESP_LOGW(TAG, "Sensor: %.2f %.1f %.2f", temp, humidity, pressure);
}
benchmark::DoNotOptimize(temp);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Logger_3Float);
} // namespace esphome::benchmarks
+233
View File
@@ -0,0 +1,233 @@
#include <benchmark/benchmark.h>
#include "esphome/core/scheduler.h"
#include "esphome/core/hal.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
// sub-microsecond benchmarks.
// Must be divisible by all batch sizes used below (3, 10) to avoid
// pool imbalance at iteration boundaries that causes spurious malloc.
static constexpr int kInnerIterations = 2100;
// Warm the scheduler pool by registering and replacing items twice.
// The first batch allocates fresh items; the second batch cancels them and
// populates the recycling pool with the cancelled items from the first batch.
static void warm_pool(Scheduler &scheduler, Component *component, int batch_size, uint32_t delay) {
uint32_t now = millis();
for (int i = 0; i < batch_size; i++) {
scheduler.set_timeout(component, static_cast<uint32_t>(i), delay, []() {});
}
scheduler.call(++now);
for (int i = 0; i < batch_size; i++) {
scheduler.set_timeout(component, static_cast<uint32_t>(i), delay, []() {});
}
scheduler.call(++now);
}
// --- Scheduler fast path: no work to do ---
static void Scheduler_Call_NoWork(benchmark::State &state) {
Scheduler scheduler;
uint32_t now = millis();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
scheduler.call(now);
}
benchmark::DoNotOptimize(now);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Scheduler_Call_NoWork);
// --- Scheduler with timers: call() when timers exist but aren't due ---
static void Scheduler_Call_TimersNotDue(benchmark::State &state) {
Scheduler scheduler;
Component dummy_component;
// Add some timeouts far in the future
for (int i = 0; i < 10; i++) {
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i), 1000000, []() {});
}
scheduler.process_to_add();
uint32_t now = millis();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
scheduler.call(now);
}
benchmark::DoNotOptimize(now);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Scheduler_Call_TimersNotDue);
// --- Scheduler with 5 intervals firing every call ---
static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) {
Scheduler scheduler;
Component dummy_component;
int fire_count = 0;
// Benchmarks the heap-based scheduler dispatch with 5 callbacks firing.
// Uses monotonically increasing fake time so intervals reliably fire every call.
// USE_BENCHMARK ifdef in component.h disables WarnIfComponentBlockingGuard
// (fake now > real millis() would cause underflow in finish()).
// interval=0 would cause an infinite loop (reschedules at same now).
for (int i = 0; i < 5; i++) {
scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i), 1, [&fire_count]() { fire_count++; });
}
scheduler.process_to_add();
uint32_t now = millis() + 100;
for (auto _ : state) {
scheduler.call(now);
now++;
benchmark::DoNotOptimize(fire_count);
}
}
BENCHMARK(Scheduler_Call_5IntervalsFiring);
// --- Scheduler: set_timeout registration ---
static void Scheduler_SetTimeout(benchmark::State &state) {
Scheduler scheduler;
Component dummy_component;
// Register 3 timeouts then call() — realistic worst case where multiple
// components schedule in the same loop iteration. warm_pool fills the
// freelist so acquire/recycle never falls back to malloc.
static constexpr int kBatchSize = 3;
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
warm_pool(scheduler, &dummy_component, kBatchSize, 1000);
for (auto _ : state) {
uint32_t now = millis();
for (int i = 0; i < kInnerIterations; i++) {
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % kBatchSize), 1000, []() {});
if ((i + 1) % kBatchSize == 0) {
scheduler.call(++now);
}
}
scheduler.call(++now);
benchmark::DoNotOptimize(scheduler);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Scheduler_SetTimeout);
// --- Scheduler: set_interval registration ---
static void Scheduler_SetInterval(benchmark::State &state) {
Scheduler scheduler;
Component dummy_component;
// Register 3 intervals then call() — realistic worst case where multiple
// components schedule in the same loop iteration. Keeps item count within
// the recycling pool (MAX_POOL_SIZE=5) to avoid spurious malloc/free.
static constexpr int kBatchSize = 3;
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
warm_pool(scheduler, &dummy_component, kBatchSize, 1000);
for (auto _ : state) {
uint32_t now = millis();
for (int i = 0; i < kInnerIterations; i++) {
scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i % kBatchSize), 1000, []() {});
if ((i + 1) % kBatchSize == 0) {
scheduler.call(++now);
}
}
scheduler.call(++now);
benchmark::DoNotOptimize(scheduler);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Scheduler_SetInterval);
// --- Scheduler: defer registration (set_timeout with delay=0) ---
static void Scheduler_Defer(benchmark::State &state) {
Scheduler scheduler;
Component dummy_component;
// defer() is Component::defer which calls set_timeout(delay=0).
// Component::defer(func) passes nullptr as the name, which skips
// cancel_item_locked_ entirely — matching production behavior where
// defers are anonymous fire-and-forget callbacks.
static constexpr int kBatchSize = 3;
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
warm_pool(scheduler, &dummy_component, kBatchSize, 0);
for (auto _ : state) {
uint32_t now = millis();
for (int i = 0; i < kInnerIterations; i++) {
scheduler.set_timeout(&dummy_component, static_cast<const char *>(nullptr), 0, []() {});
if ((i + 1) % kBatchSize == 0) {
scheduler.call(++now);
}
}
scheduler.call(++now);
benchmark::DoNotOptimize(scheduler);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Scheduler_Defer);
// --- Scheduler: defer with same ID (cancel-and-replace pattern) ---
static void Scheduler_Defer_SameID(benchmark::State &state) {
Scheduler scheduler;
Component dummy_component;
// Measures defer with a fixed numeric ID — each call cancels the previous
// pending defer before adding the new one. This is the pattern used by
// components that defer work but want to coalesce rapid updates.
static constexpr int kBatchSize = 3;
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
warm_pool(scheduler, &dummy_component, kBatchSize, 0);
for (auto _ : state) {
uint32_t now = millis();
for (int i = 0; i < kInnerIterations; i++) {
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(0), 0, []() {});
if ((i + 1) % kBatchSize == 0) {
scheduler.call(++now);
}
}
scheduler.call(++now);
benchmark::DoNotOptimize(scheduler);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Scheduler_Defer_SameID);
// --- Scheduler: set_timeout with batch size exceeding pool (cliff test) ---
static void Scheduler_SetTimeout_ExceedPool(benchmark::State &state) {
Scheduler scheduler;
Component dummy_component;
// Register 10 timeouts then call() — larger working set than the 3-item
// batches above. With the unbounded freelist, warm_pool preallocates 10
// items so this measures steady-state, not malloc cliff.
static constexpr int kBatchSize = 10;
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
warm_pool(scheduler, &dummy_component, kBatchSize, 1000);
for (auto _ : state) {
uint32_t now = millis();
for (int i = 0; i < kInnerIterations; i++) {
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % kBatchSize), 1000, []() {});
if ((i + 1) % kBatchSize == 0) {
scheduler.call(++now);
}
}
scheduler.call(++now);
benchmark::DoNotOptimize(scheduler);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Scheduler_SetTimeout_ExceedPool);
} // namespace esphome::benchmarks
+22
View File
@@ -0,0 +1,22 @@
"""Shared fixtures for the Python benchmark suite."""
from __future__ import annotations
from collections.abc import Generator
import pytest
from esphome.core import CORE
@pytest.fixture(autouse=True)
def reset_core_state() -> Generator[None]:
"""Reset CORE before and after every benchmark.
Per-iteration setups inside benchmarks reset CORE for the loop body;
this fixture handles the test-level boundary so stale state from
fixture priming doesn't leak across benchmarks.
"""
CORE.reset()
yield
CORE.reset()
@@ -0,0 +1,62 @@
substitutions:
devicename: bluetooth_proxy_device
friendly_name: bluetooth_proxy_device
esphome:
name: $devicename
friendly_name: $friendly_name
esp32:
board: esp32-poe-iso
framework:
type: esp-idf
advanced:
sram1_as_iram: true
minimum_chip_revision: "3.0"
esp32_ble_tracker:
scan_parameters:
active: false
bluetooth_proxy:
active: true
ethernet:
type: LAN8720
mdc_pin: GPIO23
mdio_pin: GPIO18
clk_mode: GPIO17_OUT
phy_addr: 0
power_pin: GPIO12
debug:
logger:
api:
ota:
platform: esphome
button:
- platform: restart
name: Restart
time:
- platform: homeassistant
id: homeassistant_time
- platform: sntp
id: sntp_time
sensor:
- platform: uptime
name: Ethernet Uptime
- platform: template
name: Free Memory
lambda: return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
unit_of_measurement: B
state_class: measurement
- platform: debug
free:
name: Heap Free
fragmentation:
name: Heap Fragmentation
min_free:
name: Heap Min Free
@@ -0,0 +1,116 @@
"""CodSpeed benchmarks for the validated-config cache fast path.
PR #16381 added a cache that lets ``esphome upload`` / ``esphome logs``
skip re-running the full config-validation pipeline. These benchmarks
compare the cached path (``load_compiled_config``) against the slow
path (``read_config``) on the same input.
The fixture YAML is a modest bluetooth-proxy device. The two paths
end up close on a config this small -- the win grows with config
complexity (external components, large package trees, deeply nested
schemas), where the slow path can be orders of magnitude slower than
the cache load.
Skipped when ``pytest-codspeed`` isn't installed so the regular
unit-test suite keeps working unchanged.
"""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import shutil
from typing import Any
import pytest
from esphome.compiled_config import compiled_config_path, load_compiled_config
from esphome.config import read_config
from esphome.core import CORE
from esphome.storage_json import ext_storage_path
from esphome.writer import update_storage_json
pytest.importorskip("pytest_codspeed")
HERE = Path(__file__).parent
FIXTURE_YAML = HERE / "fixtures" / "bluetooth_proxy_device.yaml"
def _stage_yaml(tmp_path: Path) -> Path:
"""Copy fixture YAML into a fresh tmp dir.
Each benchmark gets its own copy so the cache files (under
``.esphome/storage/`` next to the YAML) don't bleed between cases.
"""
target = tmp_path / FIXTURE_YAML.name
shutil.copy2(FIXTURE_YAML, target)
return target
def _prime_cache(yaml_path: Path) -> None:
"""Run full validation once and persist the cache + sidecar.
Mirrors ``esphome compile``: ``read_config`` populates ``CORE.config``,
then ``update_storage_json`` writes both the StorageJSON sidecar and
the ``.validated.yaml`` compiled-config cache.
"""
CORE.config_path = yaml_path
config = read_config({}, skip_external_update=True)
assert config is not None, f"fixture YAML failed to validate: {yaml_path}"
CORE.config = config
update_storage_json()
@pytest.fixture
def staged_yaml(tmp_path: Path) -> Path:
"""YAML copied into tmp_path; no cache files written yet."""
return _stage_yaml(tmp_path)
@pytest.fixture
def primed_yaml(staged_yaml: Path) -> Path:
"""YAML plus a fresh cache + sidecar on disk."""
_prime_cache(staged_yaml)
assert compiled_config_path(staged_yaml.name).is_file()
assert ext_storage_path(staged_yaml.name).is_file()
return staged_yaml
def _resetting_setup(
yaml_path: Path,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> Callable[[], tuple[tuple[Any, ...], dict[str, Any]]]:
"""Build a per-iteration setup that resets CORE and re-pins config_path."""
def setup() -> tuple[tuple[Any, ...], dict[str, Any]]:
CORE.reset()
CORE.config_path = yaml_path
return args, kwargs
return setup
def test_load_compiled_config_cached(primed_yaml: Path, benchmark) -> None:
"""Fast path: deserialize the cached, already-validated config."""
benchmark.pedantic(
load_compiled_config,
setup=_resetting_setup(primed_yaml, (primed_yaml,), {}),
rounds=5,
iterations=1,
)
def test_read_config_uncached(primed_yaml: Path, benchmark) -> None:
"""Slow path: full validation pipeline (yaml load + schema + components).
Uses the same primed fixture as the cached path -- ``read_config``
ignores the cache file on disk, so the two benchmarks measure the
same input from two different code paths.
"""
benchmark.pedantic(
read_config,
setup=_resetting_setup(primed_yaml, ({},), {"skip_external_update": True}),
rounds=3,
iterations=1,
)
@@ -0,0 +1,38 @@
// Stub for benchmark builds — provides the minimal interface that
// api_connection.cpp needs when USE_BLUETOOTH_PROXY is defined,
// without pulling in ESP32 BLE dependencies.
#pragma once
#include "esphome/components/api/api_pb2.h"
namespace esphome {
namespace api {
class APIConnection;
} // namespace api
namespace bluetooth_proxy {
class BluetoothProxy {
public:
api::APIConnection *get_api_connection() const { return nullptr; }
void subscribe_api_connection(api::APIConnection *conn, uint32_t flags) {}
void unsubscribe_api_connection(api::APIConnection *conn) {}
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg) {}
void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) {}
void bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) {}
void bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) {}
void bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) {}
void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) {}
void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) {}
void send_connections_free(api::APIConnection *conn) {}
void bluetooth_scanner_set_mode(bool active) {}
void bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg) {}
uint32_t get_feature_flags() const { return 0; }
void get_bluetooth_mac_address_pretty(char *buf) const { buf[0] = '\0'; }
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern BluetoothProxy *global_bluetooth_proxy;
} // namespace bluetooth_proxy
} // namespace esphome
@@ -0,0 +1,45 @@
// Stub for benchmark builds — provides the minimal interface that
// api_connection.cpp and Application need when USE_INFRARED is defined,
// without pulling in the real remote_base/RMT dependencies.
#pragma once
#include <cstdint>
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
namespace esphome::infrared {
class Infrared;
class InfraredCall {
public:
explicit InfraredCall(Infrared *parent) : parent_(parent) {}
InfraredCall &set_carrier_frequency(uint32_t /*frequency*/) { return *this; }
InfraredCall &set_raw_timings_packed(const uint8_t * /*data*/, uint16_t /*length*/, uint16_t /*count*/) {
return *this;
}
InfraredCall &set_repeat_count(uint32_t /*count*/) { return *this; }
void perform() {}
protected:
Infrared *parent_;
};
class InfraredTraits {
public:
uint32_t get_receiver_frequency_hz() const { return 0; }
};
class Infrared : public Component, public EntityBase {
public:
Infrared() = default;
InfraredTraits &get_traits() { return this->traits_; }
const InfraredTraits &get_traits() const { return this->traits_; }
InfraredCall make_call() { return InfraredCall(this); }
uint32_t get_capability_flags() const { return 0; }
protected:
InfraredTraits traits_;
};
} // namespace esphome::infrared
@@ -0,0 +1,51 @@
// Stub for benchmark builds — provides the minimal interface that
// api_connection.cpp and Application need when USE_RADIO_FREQUENCY is defined.
#pragma once
#include <cstdint>
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
namespace esphome::radio_frequency {
enum RadioFrequencyModulation : uint32_t {
RADIO_FREQUENCY_MODULATION_OOK = 0,
};
class RadioFrequency;
class RadioFrequencyCall {
public:
explicit RadioFrequencyCall(RadioFrequency *parent) : parent_(parent) {}
RadioFrequencyCall &set_frequency(uint32_t /*frequency*/) { return *this; }
RadioFrequencyCall &set_modulation(RadioFrequencyModulation /*mod*/) { return *this; }
RadioFrequencyCall &set_repeat_count(uint32_t /*count*/) { return *this; }
RadioFrequencyCall &set_raw_timings_packed(const uint8_t * /*data*/, uint16_t /*length*/, uint16_t /*count*/) {
return *this;
}
void perform() {}
protected:
RadioFrequency *parent_;
};
class RadioFrequencyTraits {
public:
uint32_t get_frequency_min_hz() const { return 0; }
uint32_t get_frequency_max_hz() const { return 0; }
uint32_t get_supported_modulations() const { return 0; }
};
class RadioFrequency : public Component, public EntityBase {
public:
RadioFrequency() = default;
RadioFrequencyTraits &get_traits() { return this->traits_; }
const RadioFrequencyTraits &get_traits() const { return this->traits_; }
RadioFrequencyCall make_call() { return RadioFrequencyCall(this); }
uint32_t get_capability_flags() const { return 0; }
protected:
RadioFrequencyTraits traits_;
};
} // namespace esphome::radio_frequency
@@ -0,0 +1,46 @@
// Stub for benchmark builds — provides the minimal interface that
// api_connection.cpp and Application need when USE_SERIAL_PROXY is defined,
// without pulling in the real UART implementation.
#pragma once
#include <cstdint>
#include <cstddef>
#include "esphome/components/api/api_pb2.h"
namespace esphome {
namespace api {
class APIConnection;
} // namespace api
namespace uart {
enum class UARTFlushResult : uint8_t {
UART_FLUSH_RESULT_SUCCESS,
UART_FLUSH_RESULT_ASSUMED_SUCCESS,
UART_FLUSH_RESULT_TIMEOUT,
UART_FLUSH_RESULT_FAILED,
};
} // namespace uart
namespace serial_proxy {
class SerialProxy {
public:
void set_instance_index(uint32_t index) { this->instance_index_ = index; }
uint32_t get_instance_index() const { return this->instance_index_; }
const char *get_name() const { return ""; }
api::enums::SerialProxyPortType get_port_type() const { return {}; }
api::APIConnection *get_api_connection() { return nullptr; }
void serial_proxy_request(api::APIConnection *conn, api::enums::SerialProxyRequestType type) {}
void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint32_t stop_bits, uint32_t data_size) {}
void write_from_client(const uint8_t *data, size_t len) {}
void set_modem_pins(uint32_t line_states) {}
uint32_t get_modem_pins() const { return 0; }
uart::UARTFlushResult flush_port() { return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; }
protected:
uint32_t instance_index_{0};
};
} // namespace serial_proxy
} // namespace esphome
@@ -0,0 +1,29 @@
// Stub for benchmark builds — provides the minimal interface that
// api_connection.cpp needs when USE_ZWAVE_PROXY is defined,
// without pulling in the real UART-based ZWaveProxy implementation.
#pragma once
#include "esphome/components/api/api_pb2.h"
namespace esphome {
namespace api {
class APIConnection;
} // namespace api
namespace zwave_proxy {
class ZWaveProxy {
public:
api::APIConnection *get_api_connection() { return nullptr; }
void zwave_proxy_request(api::APIConnection *conn, api::enums::ZWaveProxyRequestType type) {}
void send_frame(api::APIConnection *api_connection, const uint8_t *data, size_t length) {}
void api_connection_authenticated(api::APIConnection *conn) {}
uint32_t get_feature_flags() const { return 0; }
uint32_t get_home_id() { return 0; }
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern ZWaveProxy *global_zwave_proxy;
} // namespace zwave_proxy
} // namespace esphome
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

@@ -0,0 +1,33 @@
# New `image:` `platform: animation` form. Exercises animation/image.py through
# the real platform loader and codegen pipeline.
esphome:
name: test
esp32:
board: esp32s3box
image:
- platform: animation
id: test_animation
file: anim.gif
type: rgb565
loop:
start_frame: 0
end_frame: 2
repeat: 3
- platform: animation
id: test_animation_no_loop
file: anim.gif
type: rgb565
spi:
mosi_pin: 6
clk_pin: 7
psram:
mode: quad
display:
- platform: mipi_spi
id: lcd_display
model: s3box
@@ -0,0 +1,28 @@
# Legacy top-level `animation:` form. Exercises the deprecation shim and the
# shared codegen path through the real read_config/codegen pipeline.
esphome:
name: test
esp32:
board: esp32s3box
animation:
- id: test_animation
file: anim.gif
type: rgb565
loop:
start_frame: 0
end_frame: 2
repeat: 3
spi:
mosi_pin: 6
clk_pin: 7
psram:
mode: quad
display:
- platform: mipi_spi
id: lcd_display
model: s3box
@@ -0,0 +1,81 @@
"""Tests for the animation image platform and the legacy `animation:` shim."""
from __future__ import annotations
from collections.abc import Callable
import logging
from pathlib import Path
import pytest
from esphome.components.animation import (
DOMAIN,
LEGACY_REMOVAL_VERSION,
_capture_legacy_entry,
_warn_legacy_animation,
)
from esphome.core import CORE
from esphome.types import ConfigType
# ---------------------------------------------------------------------------
# Legacy top-level `animation:` deprecation shim -- REMOVE these tests after
# 2027.1.0 together with the shim in esphome/components/animation/__init__.py.
# ---------------------------------------------------------------------------
def test_warn_legacy_animation_warns_once(
caplog: pytest.LogCaptureFixture,
) -> None:
"""The deprecation warning fires exactly once and never mutates the config."""
config: ConfigType = {"id": "test_animation", "file": "anim.gif", "type": "rgb565"}
# A per-entry capture (CONFIG_SCHEMA step) records the raw entry so the
# one-shot warning can print a pasteable migrated block.
assert _capture_legacy_entry(config) is config
with caplog.at_level(logging.WARNING):
# First call: flag not yet set -> warns and records the flag.
assert _warn_legacy_animation(config) is config
# Second call: flag already set -> stays silent (the dedup branch).
assert _warn_legacy_animation(config) is config
assert CORE.data[DOMAIN]["legacy_warning_shown"] is True
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
assert len(warnings) == 1
assert "deprecated" in caplog.text
assert "platform: animation" in caplog.text
assert LEGACY_REMOVAL_VERSION in caplog.text
def test_legacy_animation_generation(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
caplog: pytest.LogCaptureFixture,
) -> None:
"""The legacy `animation:` block validates, warns, and generates codegen
through the real read_config/codegen pipeline."""
with caplog.at_level(logging.WARNING):
main_cpp = generate_main(component_config_path("animation_test.yaml"))
# Deprecation warning surfaced through the real validation pipeline.
assert "animation" in caplog.text
assert "deprecated" in caplog.text
# setup_animation ran: Animation object constructed and loop configured.
assert "new(test_animation) animation::Animation(" in main_cpp
assert "test_animation->set_loop(0, 2, 3);" in main_cpp
def test_animation_platform_generation(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""The `image:` `platform: animation` form generates codegen through the
real platform loader (animation/image.py) without any deprecation warning."""
main_cpp = generate_main(component_config_path("animation_platform_test.yaml"))
assert "new(test_animation) animation::Animation(" in main_cpp
assert "test_animation->set_loop(0, 2, 3);" in main_cpp
# The loop-less entry constructs the object but never configures a loop.
assert "new(test_animation_no_loop) animation::Animation(" in main_cpp
assert "test_animation_no_loop->set_loop(" not in main_cpp
@@ -0,0 +1,28 @@
"""Tests for arg-type selection of api user-defined services with homeassistant.action."""
CONFIG = "tests/component_tests/api/test_homeassistant_action.yaml"
def test_synchronous_chain_keeps_zero_copy_args(generate_main):
"""A chain of synchronous actions keeps the non-owning StringRef arg type."""
main_cpp = generate_main(CONFIG)
assert (
"api::UserServiceTrigger<api::enums::SUPPORTS_RESPONSE_NONE, StringRef>"
'("zero_copy_args", {"message"})' in main_cpp
)
def test_response_callback_args_are_owning(generate_main):
"""homeassistant.action with on_success/on_error stores the trigger args
until the HomeassistantActionResponse arrives, so string args must fall
back to owning std::string; StringRef would point into the connection's
receive buffer, which is reused before the response arrives."""
main_cpp = generate_main(CONFIG)
assert (
"api::UserServiceTrigger<api::enums::SUPPORTS_RESPONSE_NONE, std::string>"
'("response_args", {"message"})' in main_cpp
)
assert "api::HomeAssistantServiceCallAction<std::string>" in main_cpp
assert "api::HomeAssistantServiceCallAction<StringRef>" not in main_cpp
@@ -0,0 +1,43 @@
esphome:
name: test
esp32:
board: esp32dev
wifi:
ssid: SomeNetwork
password: SomePassword
logger:
api:
actions:
# Chain of synchronous actions that never store the args:
# keeps the zero-copy StringRef arg type.
- action: zero_copy_args
variables:
message: string
then:
- logger.log:
format: "%s"
args: [message.c_str()]
# homeassistant.action with on_success/on_error stores the trigger args
# until the action response arrives, so the codegen must fall back to
# owning std::string args (StringRef would dangle once the receive
# buffer is reused).
- action: response_args
variables:
message: string
then:
- homeassistant.action:
action: notify.notify
data:
message: !lambda return message;
on_success:
- logger.log:
format: "sent %s"
args: [message.c_str()]
on_error:
- logger.log:
format: "failed (%s): %s"
args: [error.c_str(), message.c_str()]
+35
View File
@@ -0,0 +1,35 @@
"""Config-validation tests for the aqi sensor component."""
import pytest
from voluptuous import Invalid
from esphome.components.aqi import CONF_CALCULATION_TYPE, CONF_EXTENDED_RANGE
from esphome.components.aqi.sensor import _validate_extended_range
def test_extended_range_rejected_with_caqi():
"""extended_range has no meaning for CAQI (no spec maximum) and must be rejected."""
with pytest.raises(Invalid, match="CAQI"):
_validate_extended_range(
{CONF_CALCULATION_TYPE: "CAQI", CONF_EXTENDED_RANGE: True}
)
def test_extended_range_rejected_with_caqi_even_when_false():
"""The option is not allowed at all with CAQI, regardless of its value."""
with pytest.raises(Invalid, match="CAQI"):
_validate_extended_range(
{CONF_CALCULATION_TYPE: "CAQI", CONF_EXTENDED_RANGE: False}
)
def test_extended_range_allowed_with_aqi():
"""extended_range is valid for the US AQI calculation."""
config = {CONF_CALCULATION_TYPE: "AQI", CONF_EXTENDED_RANGE: True}
assert _validate_extended_range(config) is config
def test_caqi_without_extended_range_ok():
"""CAQI is fine as long as extended_range is not set."""
config = {CONF_CALCULATION_TYPE: "CAQI"}
assert _validate_extended_range(config) is config
@@ -15,7 +15,7 @@ def test_binary_sensor_is_setup(generate_main):
)
# Then
assert "new gpio::GPIOBinarySensor();" in main_cpp
assert "static gpio::GPIOBinarySensor *const" in main_cpp
assert "App.register_binary_sensor" in main_cpp
@@ -31,7 +31,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main):
)
# Then
assert 'bs_1->configure_entity_("test bs1",' in main_cpp
assert 'App.register_binary_sensor(bs_1, "test bs1",' in main_cpp
assert "bs_1->set_pin(" in main_cpp
+3 -2
View File
@@ -13,7 +13,8 @@ def test_button_is_setup(generate_main):
main_cpp = generate_main("tests/component_tests/button/test_button.yaml")
# Then
assert "new wake_on_lan::WakeOnLanButton();" in main_cpp
assert "static wake_on_lan::WakeOnLanButton *const" in main_cpp
assert ") wake_on_lan::WakeOnLanButton();" in main_cpp
assert "App.register_button" in main_cpp
assert "App.register_component" in main_cpp
@@ -28,7 +29,7 @@ def test_button_sets_mandatory_fields(generate_main):
main_cpp = generate_main("tests/component_tests/button/test_button.yaml")
# Then
assert 'wol_1->configure_entity_("wol_test_1",' in main_cpp
assert 'App.register_button(wol_1, "wol_test_1",' in main_cpp
assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp
+39 -1
View File
@@ -104,6 +104,44 @@ def set_component_config() -> Callable[[str, Any], None]:
return setter
@pytest.fixture
def choose_variant_with_pins() -> Generator[Callable[[list], None]]:
"""Set the ESP32 variant to the first one on which all the given pins are valid.
For ESP32 only, since the other platforms do not have variants. The core
configuration must already have been set up for an ESP32 target.
Using local imports to avoid importing when ESP32 is not the target.
"""
from esphome import config_validation as cv
from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANTS
from esphome.components.esp32.gpio import validate_gpio_pin
from esphome.const import CONF_INPUT, CONF_OUTPUT
from esphome.pins import gpio_pin_schema
def chooser(pins: list) -> None:
for variant in VARIANTS:
try:
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
for pin in pins:
if pin is not None:
pin = gpio_pin_schema(
{
CONF_INPUT: True,
CONF_OUTPUT: True,
},
internal=True,
)(pin)
validate_gpio_pin(pin)
return
except cv.Invalid:
continue
raise cv.Invalid(
f"No compatible variant found for pins: {', '.join(map(str, pins))}"
)
yield chooser
@pytest.fixture
def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
"""Return a function to get absolute paths relative to the component's fixtures directory."""
@@ -134,7 +172,7 @@ def generate_main() -> Generator[Callable[[str | Path], str]]:
CORE.config_path = Path(path)
CORE.config = read_config({})
generate_cpp_contents(CORE.config)
return CORE.cpp_main_section
return CORE.cpp_global_section + CORE.cpp_main_section
yield generator
@@ -7,8 +7,12 @@ def test_deep_sleep_setup(generate_main):
"""
main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml")
assert "deepsleep = new deep_sleep::DeepSleepComponent();" in main_cpp
assert "App.register_component_(deepsleep);" in main_cpp
assert (
"static deep_sleep::DeepSleepComponent *const deepsleep = reinterpret_cast<deep_sleep::DeepSleepComponent *>(deep_sleep__deepsleep__pstorage);"
in main_cpp
)
assert "new(deepsleep) deep_sleep::DeepSleepComponent();" in main_cpp
assert "App.register_component_(deepsleep, " in main_cpp
def test_deep_sleep_sleep_duration(generate_main):
@@ -29,6 +33,43 @@ def test_deep_sleep_run_duration_simple(generate_main):
assert "deepsleep->set_run_duration(10000);" in main_cpp
def test_deep_sleep_on_wake_trigger(generate_main):
"""
When deep sleep is configured with a component-level on_wake automation,
a WakeTrigger component should be registered with the wakeup cause as
the automation argument.
"""
main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep3.yaml")
assert "deep_sleep::WakeTrigger();" in main_cpp
assert "Automation<deep_sleep::WakeupCause>" in main_cpp
def test_deep_sleep_ext1_on_wake_triggers(generate_main):
"""
Each esp32_ext1_wakeup pin with an on_wake automation should get its own
Ext1WakeTrigger with the pin number, and all pins (including the legacy
bare-pin shorthand) should contribute to the ext1 wakeup mask.
"""
main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep3.yaml")
assert "deep_sleep::Ext1WakeTrigger(2);" in main_cpp
assert "deep_sleep::Ext1WakeTrigger(4);" in main_cpp
# GPIO13 has no on_wake, so no trigger is created for it
assert "deep_sleep::Ext1WakeTrigger(13)" not in main_cpp
# mask covers GPIO2, GPIO4 and GPIO13
assert ".mask = 8212," in main_cpp
def test_deep_sleep_no_on_wake_no_triggers(generate_main):
"""
Without any on_wake automations, no wake trigger code should be generated.
"""
main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml")
assert "WakeTrigger" not in main_cpp
def test_deep_sleep_run_duration_dictionary(generate_main):
"""
When deep sleep is configured with dictionary run duration, it should be set.
@@ -0,0 +1,23 @@
esphome:
name: test
esp32:
board: nodemcu-32s
deep_sleep:
id: deepsleep
sleep_duration: 1min
run_duration: 10s
on_wake:
- lambda: 'ESP_LOGD("test", "cause %d", static_cast<int>(cause));'
esp32_ext1_wakeup:
mode: ANY_HIGH
pins:
- pin: GPIO2
on_wake:
- lambda: 'ESP_LOGD("test", "left");'
- pin:
number: GPIO4
on_wake:
- lambda: 'ESP_LOGD("test", "right");'
- number: GPIO13
@@ -0,0 +1,148 @@
"""Tests for display component metadata functions."""
from unittest.mock import patch
import pytest
from esphome.components.const import BYTE_ORDER_BIG, BYTE_ORDER_LITTLE
from esphome.components.display import (
DisplayMetaData,
add_metadata,
get_all_display_metadata,
get_display_metadata,
)
from esphome.config import Config
from esphome.core import ID
from esphome.final_validate import full_config
def test_add_metadata_basic():
"""Test adding metadata with an ID object."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata(ID("my_display"), 320, 240)
meta = get_display_metadata(ID("my_display"))
assert meta == DisplayMetaData(
width=320,
height=240,
has_hardware_rotation=False,
byte_order=BYTE_ORDER_BIG,
)
def test_add_metadata_with_all_fields():
"""Test adding metadata with all fields set."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata(
ID("my_display"),
480,
320,
has_hardware_rotation=True,
byte_order=BYTE_ORDER_LITTLE,
)
meta = get_display_metadata(ID("my_display"))
assert meta == DisplayMetaData(
width=480,
height=320,
has_hardware_rotation=True,
byte_order=BYTE_ORDER_LITTLE,
)
def test_add_metadata_hardware_rotation_default():
"""Test that has_hardware_rotation defaults to False."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata(ID("disp"), 128, 64)
meta = get_display_metadata(ID("disp"))
assert meta.has_hardware_rotation is False
assert meta.byte_order == BYTE_ORDER_BIG
def test_add_metadata_with_byte_order():
"""Test adding metadata with explicit byte_order."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata(ID("disp"), 240, 320, byte_order=BYTE_ORDER_LITTLE)
meta = get_display_metadata(ID("disp"))
assert meta.byte_order == BYTE_ORDER_LITTLE
def test_get_display_metadata_missing_reads_raw_config():
"""Querying a non-existent ID falls back to raw config lookup."""
with patch("esphome.components.display.CORE.data", {}):
# Set up a minimal full_config with a display entry so the fallback
# path in get_display_metadata can find the display config.
fc = Config()
fc["display"] = [
{
"id": ID("no_such_display", True),
"auto_clear_enabled": True,
"dimensions": {"width": 320, "height": 240},
"byte_order": BYTE_ORDER_LITTLE,
"rotation": 90,
},
{
"id": ID("other_display", True),
"auto_clear_enabled": "undefined",
"dimensions": (1024, 600),
},
]
fc.declare_ids.append((ID("no_such_display", True), ["display", 0, "id"]))
fc.declare_ids.append((ID("other_display", True), ["display", 1, "id"]))
full_config.set(fc)
data = get_display_metadata(ID("no_such_display"))
assert data.width == 320
assert data.height == 240
assert data.has_hardware_rotation is False
assert data.has_writer is True
assert data.byte_order == BYTE_ORDER_LITTLE
assert data.rotation == 90
data = get_display_metadata(ID("other_display"))
assert data.width == 1024
assert data.height == 600
assert data.has_writer is False
def test_add_multiple_displays():
"""Test adding metadata for multiple displays."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata(ID("disp_a"), 320, 240)
add_metadata(ID("disp_b"), 128, 64, has_hardware_rotation=True)
all_meta = get_all_display_metadata()
assert len(all_meta) == 2
assert all_meta["disp_a"] == DisplayMetaData(320, 240, False)
assert all_meta["disp_b"] == DisplayMetaData(128, 64, True, BYTE_ORDER_BIG)
def test_add_duplicate_id_asserts():
"""Adding metadata for the same ID object twice should assert."""
with patch("esphome.components.display.CORE.data", {}):
id_obj = ID("disp")
add_metadata(id_obj, 320, 240)
with pytest.raises(AssertionError, match="Duplicate"):
add_metadata(id_obj, 640, 480)
def test_metadata_is_frozen():
"""Test that DisplayMetaData instances are immutable (frozen dataclass)."""
meta = DisplayMetaData(320, 240, False, BYTE_ORDER_BIG)
with pytest.raises(AttributeError):
meta.width = 640
with pytest.raises(AttributeError):
meta.byte_order = BYTE_ORDER_LITTLE
def test_get_all_metadata_asserts_on_unresolved_id():
"""get_all_display_metadata should assert if any ID has id=None."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata(ID(None), 320, 240)
with pytest.raises(AssertionError, match="resolved"):
get_all_display_metadata()
def test_get_metadata_asserts_on_unresolved_id():
"""get_display_metadata should assert if any ID has id=None."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata(ID(None), 320, 240)
with pytest.raises(AssertionError, match="resolved"):
get_display_metadata(ID("anything"))
@@ -0,0 +1,24 @@
esphome:
name: test
esp32:
board: esp32dev
spi:
clk_pin: GPIO18
mosi_pin: GPIO19
display:
- platform: epaper_spi
id: epaper_display
model: ssd1677
dc_pin: GPIO21
busy_pin: GPIO22
reset_pin: GPIO23
cs_pin: GPIO5
enable_pin:
- GPIO25
- GPIO26
dimensions:
width: 200
height: 200
@@ -0,0 +1,156 @@
"""Tests for display metadata created by the epaper_spi component."""
from collections.abc import Callable
from pathlib import Path
from typing import Any
from esphome import config_validation as cv
from esphome.components.display import get_all_display_metadata, get_display_metadata
from esphome.components.epaper_spi.display import CONFIG_SCHEMA
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32
from esphome.const import PlatformFramework
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
def _base_config(**overrides: Any) -> ConfigType:
"""Build a minimal valid ssd1677 config, allowing field overrides."""
config: ConfigType = {
"id": "test_display",
"model": "ssd1677",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
"dimensions": {"width": 200, "height": 300},
}
config.update(overrides)
return config
def test_metadata_dimensions_and_defaults(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Metadata picks up explicit dimensions and epaper_spi defaults."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
config = CONFIG_SCHEMA(_base_config())
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.width == 200
assert meta.height == 300
# epaper_spi always reports full hardware rotation
assert meta.has_hardware_rotation is True
# epaper_spi does not declare a byte order
assert meta.byte_order is cv.UNDEFINED
assert meta.draw_rounding == 0
# no drawing methods configured -> no writer
assert meta.has_writer is False
def test_metadata_default_dimensions_from_model(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""A model with built-in dimensions reports those without explicit dimensions."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
# waveshare-4.26in is an ssd1677 derivative with default 800x480 dimensions
config = CONFIG_SCHEMA(
{
"id": "wave_display",
"model": "waveshare-4.26in",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
}
)
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.width == 800
assert meta.height == 480
def test_metadata_has_writer_with_auto_clear(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""A display with auto_clear_enabled reports has_writer=True."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
config = CONFIG_SCHEMA(_base_config(auto_clear_enabled=True))
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.has_writer is True
def test_metadata_rotation_propagated(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""The configured rotation is stored in the metadata."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
config = CONFIG_SCHEMA(_base_config(rotation=90))
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.rotation == 90
def test_metadata_multiple_displays_independent(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Each display gets its own independent metadata entry."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
CONFIG_SCHEMA(_base_config(id="disp_a", dimensions={"width": 200, "height": 300}))
CONFIG_SCHEMA(_base_config(id="disp_b", dimensions={"width": 400, "height": 480}))
all_meta = get_all_display_metadata()
assert all_meta["disp_a"].width == 200
assert all_meta["disp_a"].height == 300
assert all_meta["disp_b"].width == 400
assert all_meta["disp_b"].height == 480
def test_metadata_via_code_generation(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Full code generation registers metadata for the configured display."""
generate_main(component_config_path("enable_pin_test.yaml"))
all_meta = get_all_display_metadata()
assert len(all_meta) == 1
meta = next(iter(all_meta.values()))
# enable_pin_test.yaml: ssd1677 at 200x200
assert meta.width == 200
assert meta.height == 200
assert meta.has_hardware_rotation is True
+159 -39
View File
@@ -1,6 +1,8 @@
"""Tests for epaper_spi configuration validation."""
from collections.abc import Callable
from pathlib import Path
import re
from typing import Any
import pytest
@@ -11,17 +13,13 @@ from esphome.components.epaper_spi.display import (
FINAL_VALIDATE_SCHEMA,
MODELS,
)
from esphome.components.esp32 import (
KEY_BOARD,
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32S3,
)
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32
from esphome.const import (
CONF_BUSY_PIN,
CONF_CS_PIN,
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
CONF_HEIGHT,
CONF_INIT_SEQUENCE,
CONF_RESET_PIN,
@@ -31,6 +29,30 @@ from esphome.const import (
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
# Pin options whose values must be valid on the chosen ESP32 variant.
_PIN_CONF_KEYS = (
CONF_CS_PIN,
CONF_DC_PIN,
CONF_RESET_PIN,
CONF_BUSY_PIN,
CONF_ENABLE_PIN,
)
def _pins_for(model: Any, config: ConfigType) -> list:
"""Collect every GPIO the config will actually use (model defaults or injected)."""
pins: list = []
for key in _PIN_CONF_KEYS:
# An injected value in the config takes precedence over the model default.
value = config[key] if key in config else model.get_default(key)
if not value: # get_default returns False for pins the model omits
continue
if isinstance(value, list):
pins.extend(value)
else:
pins.append(value)
return pins
def run_schema_validation(
config: ConfigType, with_final_validate: bool = False
@@ -90,29 +112,20 @@ def test_basic_configuration_errors(
def test_all_predefined_models(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
choose_variant_with_pins: Callable[[list], None],
) -> None:
"""Test all predefined epaper models validate successfully with appropriate defaults."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
# Test all models, providing default values where necessary
for name, model in MODELS.items():
# SEEED models are designed for ESP32-S3 hardware
if name in ("SEEED-EE04-MONO-4.26", "SEEED-RETERMINAL-E1002"):
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={
KEY_BOARD: "esp32-s3-devkitc-1",
KEY_VARIANT: VARIANT_ESP32S3,
},
)
else:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
config = {"model": name}
# Add ID field
@@ -141,6 +154,14 @@ def test_all_predefined_models(
if not model.get_default(CONF_CS_PIN):
config[CONF_CS_PIN] = 5
# Dual-CS models (e.g. T133A01) require a second chip-select pin
if model.manages_cs and not model.get_default("cs1_pin"):
config["cs1_pin"] = 4
# Select an ESP32 variant on which all of this model's pins are valid
# (some models default to high-numbered pins only present on the S3).
choose_variant_with_pins(_pins_for(model, config))
run_schema_validation(config)
@@ -152,27 +173,19 @@ def test_individual_models(
model_name: str,
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
choose_variant_with_pins: Callable[[list], None],
) -> None:
"""Test each epaper model individually to ensure it validates correctly."""
# SEEED models are designed for ESP32-S3 hardware
if model_name in ("SEEED-EE04-MONO-4.26", "SEEED-RETERMINAL-E1002"):
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={
KEY_BOARD: "esp32-s3-devkitc-1",
KEY_VARIANT: VARIANT_ESP32S3,
},
)
else:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
model = MODELS[model_name]
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
model = MODELS[model_name]
config: dict[str, Any] = {"model": model_name, "id": "test_display"}
# Add required fields based on model defaults
@@ -195,6 +208,14 @@ def test_individual_models(
if not model.get_default(CONF_CS_PIN):
config[CONF_CS_PIN] = 5
# Dual-CS models (e.g. T133A01) require a second chip-select pin
if model.manages_cs and not model.get_default("cs1_pin"):
config["cs1_pin"] = 4
# Select an ESP32 variant on which all of this model's pins are valid
# (some models default to high-numbered pins only present on the S3).
choose_variant_with_pins(_pins_for(model, config))
# This should not raise any exceptions
run_schema_validation(config)
@@ -342,3 +363,102 @@ def test_busy_pin_input_mode_ssd1677(
reset_pin_config = result[CONF_RESET_PIN]
assert "mode" in reset_pin_config
assert reset_pin_config["mode"]["output"] is True
def test_enable_pin_single(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test that a single enable_pin is accepted and normalised to a list of output pins."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
result = run_schema_validation(
{
"id": "test_display",
"model": "ssd1677",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
"enable_pin": 25,
"dimensions": {
"width": 200,
"height": 200,
},
}
)
# A single pin is normalised to a list by cv.ensure_list
assert CONF_ENABLE_PIN in result
enable_pins = result[CONF_ENABLE_PIN]
assert isinstance(enable_pins, list)
assert len(enable_pins) == 1
# enable pins are configured as outputs
assert enable_pins[0]["mode"]["output"] is True
def test_enable_pin_multiple(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test that a list of enable_pins is accepted."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
result = run_schema_validation(
{
"id": "test_display",
"model": "ssd1677",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
"enable_pin": [25, 26],
"dimensions": {
"width": 200,
"height": 200,
},
}
)
assert CONF_ENABLE_PIN in result
enable_pins = result[CONF_ENABLE_PIN]
assert isinstance(enable_pins, list)
assert len(enable_pins) == 2
assert all(pin["mode"]["output"] is True for pin in enable_pins)
def test_enable_pin_code_generation(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Test that enable_pins are wired up in the generated C++ code."""
main_cpp = generate_main(component_config_path("enable_pin_test.yaml"))
# Derive the auto-generated pin variable names from the set_pin() lines
# rather than hard-coding them, so the test does not break when unrelated
# codegen details shift the generated IDs.
def pin_var_for(gpio_num: int) -> str:
match = re.search(rf"(\w+)->set_pin\(::GPIO_NUM_{gpio_num}\);", main_cpp)
assert match is not None, (
f"GPIO_NUM_{gpio_num} pin not set up in generated code"
)
return match.group(1)
pin_25 = pin_var_for(25)
pin_26 = pin_var_for(26)
# Both pin objects must be passed to the display via set_enable_pins() as a
# std::vector initializer list, in the configured order.
assert f"set_enable_pins({{{pin_25}, {pin_26}}});" in main_cpp
@@ -0,0 +1,7 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
@@ -0,0 +1,10 @@
esphome:
name: test
esp32:
board: esp32dev
flash_mode: qio
flash_frequency: 80MHz
toolchain: platformio
framework:
type: esp-idf
@@ -0,0 +1,17 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
ethernet:
type: W5500
clk_pin: 19
mosi_pin: 21
miso_pin: 23
cs_pin: 18
interrupt_pin: 36
reset_pin: 22
clock_speed: 10Mhz
@@ -0,0 +1,14 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: "test_ssid"
password: "test_password"
esp32_ble_tracker:
software_coexistence: true
@@ -0,0 +1,26 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: "test_ssid"
password: "test_password"
ethernet:
type: W5500
clk_pin: 19
mosi_pin: 21
miso_pin: 23
cs_pin: 18
interrupt_pin: 36
reset_pin: 22
clock_speed: 10Mhz
network:
priority:
- ethernet
- wifi
@@ -0,0 +1,11 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: "test_ssid"
password: "test_password"
@@ -0,0 +1,10 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
advanced:
nvs_encryption:
key_id: 0
@@ -0,0 +1,16 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
psram:
mode: octal
disabled: true
binary_sensor:
- platform: gpio
pin: GPIO34
name: test
@@ -0,0 +1,15 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
psram:
mode: octal
binary_sensor:
- platform: gpio
pin: GPIO34
name: test
@@ -0,0 +1,15 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
psram:
mode: quad
binary_sensor:
- platform: gpio
pin: GPIO34
name: test

Some files were not shown because too many files have changed in this diff Show More