Merge remote-tracking branch 'upstream/dev' into integration

This commit is contained in:
J. Nick Koston
2026-03-22 18:46:46 -10:00
9 changed files with 211 additions and 10 deletions
+2 -2
View File
@@ -366,14 +366,14 @@ class MQTTJsonMessageTrigger : public Trigger<JsonObjectConst> {
class MQTTConnectTrigger : public Trigger<bool> {
public:
explicit MQTTConnectTrigger(MQTTClientComponent *&client) {
explicit MQTTConnectTrigger(MQTTClientComponent *client) {
client->set_on_connect([this](bool session_present) { this->trigger(session_present); });
}
};
class MQTTDisconnectTrigger : public Trigger<MQTTClientDisconnectReason> {
public:
explicit MQTTDisconnectTrigger(MQTTClientComponent *&client) {
explicit MQTTDisconnectTrigger(MQTTClientComponent *client) {
client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(reason); });
}
};
+24 -8
View File
@@ -565,6 +565,29 @@ def new_variable(
return obj
def _extract_component_ns(type_str: str) -> str:
"""Extract the component namespace from a fully-qualified C++ type string.
Strips leading ``esphome::`` and template arguments, then returns
the first namespace segment. Falls back to ``"esphome"`` when the
type has no namespace qualifier (after stripping templates).
Examples::
esphome::dsmr::Dsmr -> dsmr
esphome::logger::Logger -> logger
esphome::Automation<std::optional<bool>, std::optional<bool>> -> esphome
Logger -> esphome
"""
bare = type_str.removeprefix("esphome::")
# Strip template arguments before namespace extraction to avoid
# matching :: inside template params (e.g. Automation<std::optional<bool>>)
bare_no_template = bare.split("<", maxsplit=1)[0]
if "::" in bare_no_template:
return bare_no_template.split("::", maxsplit=1)[0].rstrip("_")
return "esphome"
def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
"""Declare a new pointer variable in the code generation.
@@ -585,14 +608,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
# to avoid heap fragmentation on embedded devices.
the_type = id_.type
# Extract component namespace from type for memory analysis attribution
type_str = str(the_type)
# Strip leading esphome:: to get the component namespace
# e.g. esphome::dsmr::Dsmr -> dsmr, logger::Logger -> logger
bare = type_str.removeprefix("esphome::")
if "::" in bare:
component_ns = bare.split("::", maxsplit=1)[0].rstrip("_")
else:
component_ns = "esphome"
component_ns = _extract_component_ns(str(the_type))
storage_name = f"{component_ns}__{id_.id}__pstorage"
# Declare aligned byte array for the object storage
@@ -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,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(1.0f),
new MultiplyFilter(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 @@
sensor:
+27
View File
@@ -1,6 +1,7 @@
import pytest
from esphome import codegen as cg
from esphome.cpp_generator import _extract_component_ns
# Test interface remains the same.
@@ -75,3 +76,29 @@ from esphome import codegen as cg
)
def test_exists(attr):
assert hasattr(cg, attr)
@pytest.mark.parametrize(
("type_str", "expected"),
(
("esphome::dsmr::Dsmr", "dsmr"),
("esphome::logger::Logger", "logger"),
("esphome::web_server::WebServer", "web_server"),
("esphome::deep_sleep::DeepSleep", "deep_sleep"),
("esphome::Component", "esphome"),
("Logger", "esphome"),
# Template types with :: in template args must not confuse extraction
(
"esphome::Automation<std::optional<bool>, std::optional<bool>>",
"esphome",
),
(
"esphome::StatelessLambdaAction<std::optional<bool>, std::optional<bool>>",
"esphome",
),
# Namespaced template type
("esphome::sensor::Sensor<std::string>", "sensor"),
),
)
def test_extract_component_ns(type_str, expected):
assert _extract_component_ns(type_str) == expected