mirror of
https://github.com/esphome/esphome.git
synced 2026-09-15 09:08:41 +00:00
Merge branch 'dev' into api-sint32-short-varint
This commit is contained in:
+172
-2
@@ -239,6 +239,123 @@ This document provides essential context for AI models interacting with this pro
|
||||
var = await switch.new_switch(config)
|
||||
```
|
||||
|
||||
* **Automations (Triggers, Actions, Conditions):**
|
||||
|
||||
Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true).
|
||||
|
||||
* **Triggers -- Callback method (preferred):**
|
||||
|
||||
Use `build_callback_automation()` for simple triggers. This eliminates the need for a C++ Trigger class by using a lightweight pointer-sized forwarder struct registered directly as a callback. No `CONF_TRIGGER_ID` in the schema.
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
from esphome import automation
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema({
|
||||
cv.GenerateID(): cv.declare_id(MyComponent),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||
}).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_state_callback", [(bool, "x")], conf
|
||||
)
|
||||
```
|
||||
|
||||
`build_callback_automation` arguments: `parent`, `callback_method` (C++ method name), `args` (template args as `[(type, name)]` tuples), `config`, and optional `forwarder` (defaults to `TriggerForwarder<Ts...>`).
|
||||
|
||||
For boolean filtering (e.g. `on_press`/`on_release`), use built-in forwarders with `args=[]`:
|
||||
```python
|
||||
for conf_key, forwarder in (
|
||||
(CONF_ON_PRESS, automation.TriggerOnTrueForwarder),
|
||||
(CONF_ON_RELEASE, automation.TriggerOnFalseForwarder),
|
||||
):
|
||||
for conf in config.get(conf_key, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_state_callback", [], conf, forwarder=forwarder
|
||||
)
|
||||
```
|
||||
|
||||
**C++ -- no trigger class needed.** The callback registration method must be templatized to accept both `std::function` and lightweight forwarder structs (which avoid heap allocation):
|
||||
```cpp
|
||||
class MyComponent : public Component {
|
||||
public:
|
||||
// Must be a template -- accepts both std::function and pointer-sized forwarder structs
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
protected:
|
||||
// Use CallbackManager when callbacks are always registered (e.g. core components)
|
||||
CallbackManager<void(bool)> state_callback_;
|
||||
// Use LazyCallbackManager when callbacks are often not registered -- saves 8 bytes
|
||||
// (nullptr vs empty std::vector) per instance when no callbacks are added
|
||||
// LazyCallbackManager<void(bool)> state_callback_;
|
||||
};
|
||||
```
|
||||
|
||||
* **Triggers -- Trigger class method:**
|
||||
|
||||
Use `build_automation()` with a `Trigger<Ts...>` subclass only when the forwarder needs **mutable state beyond a single `Automation*` pointer** (e.g. edge detection tracking previous state, timing logic).
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
TurnOnTrigger = my_ns.class_("TurnOnTrigger", automation.Trigger.template())
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema({
|
||||
cv.Optional(CONF_ON_TURN_ON): automation.validate_automation(
|
||||
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger)}
|
||||
),
|
||||
})
|
||||
|
||||
async def to_code(config):
|
||||
for conf in config.get(CONF_ON_TURN_ON, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
```
|
||||
|
||||
**C++:**
|
||||
```cpp
|
||||
class TurnOnTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit TurnOnTrigger(MyComponent *parent) : last_on_{false} {
|
||||
parent->add_on_state_callback([this](bool state) {
|
||||
if (state && !this->last_on_)
|
||||
this->trigger();
|
||||
this->last_on_ = state;
|
||||
});
|
||||
}
|
||||
protected:
|
||||
bool last_on_;
|
||||
};
|
||||
```
|
||||
|
||||
* **Actions:**
|
||||
```cpp
|
||||
template<typename... Ts> class MyAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit MyAction(MyComponent *parent) : parent_(parent) {}
|
||||
void play(const Ts &...) override { this->parent_->do_something(); }
|
||||
protected:
|
||||
MyComponent *parent_;
|
||||
};
|
||||
```
|
||||
Register with `@automation.register_action("my_component.do_something", MyAction, schema, synchronous=True)`. Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.
|
||||
|
||||
* **Conditions:**
|
||||
```cpp
|
||||
template<typename... Ts> class MyCondition : public Condition<Ts...> {
|
||||
public:
|
||||
explicit MyCondition(MyComponent *parent) : parent_(parent) {}
|
||||
bool check(const Ts &...) override { return this->parent_->is_active(); }
|
||||
protected:
|
||||
MyComponent *parent_;
|
||||
};
|
||||
```
|
||||
Register with `@automation.register_condition("my_component.is_active", MyCondition, schema)`.
|
||||
|
||||
* **Configuration Validation:**
|
||||
* **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`.
|
||||
* **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
|
||||
@@ -274,10 +391,39 @@ This document provides essential context for AI models interacting with this pro
|
||||
* **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows:
|
||||
```
|
||||
tests/
|
||||
├── test_build_components/ # Base test configurations
|
||||
└── components/[component]/ # Component-specific tests
|
||||
├── test_build_components/
|
||||
│ └── common/ # Shared bus packages (uart, i2c, spi, etc.)
|
||||
│ ├── uart/ # UART at default baud rate
|
||||
│ ├── uart_115200/ # UART at 115200 baud
|
||||
│ ├── i2c/ # I2C bus
|
||||
│ └── spi/ # SPI bus
|
||||
└── components/[component]/
|
||||
├── common.yaml # Component-only config (no bus definitions)
|
||||
├── test.esp32-idf.yaml
|
||||
├── test.esp8266-ard.yaml
|
||||
└── test.rp2040-ard.yaml
|
||||
```
|
||||
Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.
|
||||
|
||||
* **Test Grouping with Packages:** Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define buses (uart, i2c, spi, modbus) directly in test YAML files** — always use packages from `test_build_components/common/`:
|
||||
```yaml
|
||||
# test.esp32-idf.yaml — use packages for buses
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
```
|
||||
```yaml
|
||||
# common.yaml — component config only, NO bus definitions
|
||||
my_component:
|
||||
id: my_instance
|
||||
|
||||
sensor:
|
||||
- platform: my_component
|
||||
name: My Sensor
|
||||
```
|
||||
Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing CI build time.
|
||||
|
||||
* **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
|
||||
```bash
|
||||
./script/test_component_grouping.py -e config --all
|
||||
@@ -417,6 +563,30 @@ This document provides essential context for AI models interacting with this pro
|
||||
|
||||
Note: Avoiding heap allocation after `setup()` is always required regardless of component type. The prioritization above is about the effort spent on container optimization (e.g., migrating from `std::vector` to `StaticVector`).
|
||||
|
||||
**Callback Managers:**
|
||||
|
||||
ESPHome provides two callback manager types in `esphome/core/helpers.h` for the observer pattern. Both support `std::function`, lambdas, and lightweight forwarder structs via their templatized `add()` method.
|
||||
|
||||
| Type | Idle overhead (32-bit) | When to use |
|
||||
|------|----------------------|-------------|
|
||||
| `CallbackManager<void(Ts...)>` | 12 bytes (empty `std::vector`) | Callbacks are always or almost always registered |
|
||||
| `LazyCallbackManager<void(Ts...)>` | 4 bytes (`nullptr`) | Callbacks are often not registered (common case) |
|
||||
|
||||
`LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first callback is added. Prefer it for entity-level callbacks where most instances have no subscribers.
|
||||
|
||||
**Important:** Registration methods that add to a callback manager **must always be templatized** to accept both `std::function` and pointer-sized forwarder structs (used by `build_callback_automation`). Never use `std::function` in the method signature:
|
||||
```cpp
|
||||
// Bad -- forces heap allocation for forwarder structs
|
||||
void add_on_state_callback(std::function<void(bool)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
// Good -- accepts any callable without forcing std::function wrapping
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
```
|
||||
|
||||
* **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals.
|
||||
|
||||
**Bad Pattern (Module-Level Globals):**
|
||||
|
||||
@@ -32,13 +32,6 @@ void BinarySensor::publish_initial_state(bool new_state) {
|
||||
this->invalidate_state();
|
||||
this->publish_state(new_state);
|
||||
}
|
||||
void BinarySensor::send_state_internal(bool new_state) {
|
||||
// copy the new state to the visible property for backwards compatibility, before any callbacks
|
||||
this->state = new_state;
|
||||
// Note that set_new_state_ de-dups and will only trigger callbacks if the state has actually changed
|
||||
this->set_new_state(new_state);
|
||||
}
|
||||
|
||||
bool BinarySensor::set_new_state(const optional<bool> &new_state) {
|
||||
if (StatefulEntityBase::set_new_state(new_state)) {
|
||||
// weirdly, this file could be compiled even without USE_BINARY_SENSOR defined
|
||||
|
||||
@@ -32,7 +32,10 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
|
||||
*/
|
||||
class BinarySensor : public StatefulEntityBase<bool> {
|
||||
public:
|
||||
explicit BinarySensor(){};
|
||||
explicit BinarySensor() = default;
|
||||
|
||||
const bool &get_state() const override { return this->state; }
|
||||
void set_trigger_on_initial_state(bool value) { this->trigger_on_initial_state_ = value; }
|
||||
|
||||
/** Publish a new state to the front-end.
|
||||
*
|
||||
@@ -54,16 +57,24 @@ class BinarySensor : public StatefulEntityBase<bool> {
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
void send_state_internal(bool new_state);
|
||||
void send_state_internal(bool new_state) {
|
||||
// Fast path: skip virtual dispatch when state hasn't changed
|
||||
if (this->flags_.has_state && this->state == new_state)
|
||||
return;
|
||||
this->set_new_state(new_state);
|
||||
}
|
||||
|
||||
/// Return whether this binary sensor has outputted a state.
|
||||
virtual bool is_status_binary_sensor() const;
|
||||
|
||||
// For backward compatibility, provide an accessible property
|
||||
|
||||
/// The current state of this binary sensor. Also used as the backing storage for StatefulEntityBase.
|
||||
bool state{};
|
||||
|
||||
protected:
|
||||
bool get_trigger_on_initial_state() const override { return this->trigger_on_initial_state_; }
|
||||
void set_state_value(const bool &value) override { this->state = value; }
|
||||
|
||||
bool trigger_on_initial_state_{true};
|
||||
#ifdef USE_BINARY_SENSOR_FILTER
|
||||
Filter *filter_list_{nullptr};
|
||||
#endif
|
||||
@@ -73,7 +84,7 @@ class BinarySensor : public StatefulEntityBase<bool> {
|
||||
|
||||
class BinarySensorInitiallyOff : public BinarySensor {
|
||||
public:
|
||||
bool has_state() const override { return true; }
|
||||
BinarySensorInitiallyOff() { this->set_has_state(true); }
|
||||
};
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@@ -18,8 +19,9 @@ from esphome.const import (
|
||||
PLATFORM_ESP8266,
|
||||
ThreadModel,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.helpers import copy_file_if_changed
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .boards import BOARDS, ESP8266_LD_SCRIPTS
|
||||
from .const import (
|
||||
@@ -40,12 +42,42 @@ from .const import (
|
||||
)
|
||||
from .gpio import PinInitialState, add_pin_initial_states_array
|
||||
|
||||
CONF_ENABLE_SCANF_FLOAT = "enable_scanf_float"
|
||||
# Heuristically matches scanf/sscanf calls with float format specifiers.
|
||||
# Standard scanf float conversions: %f %F %e %E %g %G %a %A
|
||||
# With optional modifiers: %*f (suppression), %8f (width), %lf %Lf (length)
|
||||
# Also matches non-standard patterns like %.2f as a heuristic — these are
|
||||
# invalid in scanf but users may write them by analogy with printf.
|
||||
# Uses [^;]*? to stay within a single statement, preventing false positives
|
||||
# from e.g. sscanf(buf, "%d", &x); printf("%f", val);
|
||||
_SCANF_FLOAT_RE = re.compile(r"scanf\s*\([^;]*?%[*\d.]*[hlL]*[feEgGaAF]")
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
AUTO_LOAD = ["preferences"]
|
||||
IS_TARGET_PLATFORM = True
|
||||
|
||||
|
||||
def lambdas_use_scanf_float(config: ConfigType) -> bool:
|
||||
"""Check if any lambda in the config uses scanf with a float format specifier.
|
||||
|
||||
Comments are stripped before matching to avoid false positives from
|
||||
commented-out code. The cost of a false positive is only ~8KB flash.
|
||||
"""
|
||||
stack: list = [config]
|
||||
while stack:
|
||||
obj = stack.pop()
|
||||
if isinstance(obj, Lambda):
|
||||
src = obj.comment_remover(obj.value)
|
||||
if _SCANF_FLOAT_RE.search(src):
|
||||
return True
|
||||
elif isinstance(obj, dict):
|
||||
stack.extend(obj.values())
|
||||
elif isinstance(obj, list):
|
||||
stack.extend(obj)
|
||||
return False
|
||||
|
||||
|
||||
def set_core_data(config):
|
||||
CORE.data[KEY_ESP8266] = {}
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP8266
|
||||
@@ -181,6 +213,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ENABLE_SERIAL): cv.boolean,
|
||||
cv.Optional(CONF_ENABLE_SERIAL1): cv.boolean,
|
||||
cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ENABLE_SCANF_FLOAT): cv.boolean,
|
||||
}
|
||||
),
|
||||
set_core_data,
|
||||
@@ -201,16 +234,23 @@ async def to_code(config):
|
||||
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
||||
cg.add_define(ThreadModel.SINGLE)
|
||||
|
||||
cg.add_platformio_option(
|
||||
"extra_scripts",
|
||||
[
|
||||
"pre:testing_mode.py",
|
||||
"pre:exclude_updater.py",
|
||||
"pre:exclude_waveform.py",
|
||||
"pre:remove_float_scanf.py",
|
||||
"post:post_build.py",
|
||||
],
|
||||
)
|
||||
enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT)
|
||||
if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config):
|
||||
enable_scanf_float = True
|
||||
_LOGGER.warning(
|
||||
"Lambda uses scanf with a float format specifier; "
|
||||
"enabling scanf float support (~8KB flash)"
|
||||
)
|
||||
|
||||
extra_scripts = [
|
||||
"pre:testing_mode.py",
|
||||
"pre:exclude_updater.py",
|
||||
"pre:exclude_waveform.py",
|
||||
]
|
||||
if not enable_scanf_float:
|
||||
extra_scripts.append("pre:remove_float_scanf.py")
|
||||
extra_scripts.append("post:post_build.py")
|
||||
cg.add_platformio_option("extra_scripts", extra_scripts)
|
||||
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
cg.add_platformio_option("framework", "arduino")
|
||||
|
||||
@@ -381,7 +381,7 @@ async def filter_out_filter_to_code(config, filter_id):
|
||||
if not isinstance(config, list):
|
||||
config = [config]
|
||||
template_ = [await cg.templatable(x, [], float) for x in config]
|
||||
return cg.new_Pvariable(filter_id, template_)
|
||||
return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(template_)), template_)
|
||||
|
||||
|
||||
QUANTILE_SCHEMA = cv.All(
|
||||
@@ -650,7 +650,9 @@ async def throttle_with_priority_filter_to_code(config, filter_id):
|
||||
if not isinstance(config[CONF_VALUE], list):
|
||||
config[CONF_VALUE] = [config[CONF_VALUE]]
|
||||
template_ = [await cg.templatable(x, [], float) for x in config[CONF_VALUE]]
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_)
|
||||
return cg.new_Pvariable(
|
||||
filter_id, cg.TemplateArguments(len(template_)), config[CONF_TIMEOUT], template_
|
||||
)
|
||||
|
||||
|
||||
HEARTBEAT_SCHEMA = cv.Schema(
|
||||
|
||||
@@ -222,16 +222,14 @@ MultiplyFilter::MultiplyFilter(TemplatableValue<float> multiplier) : multiplier_
|
||||
|
||||
optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
|
||||
|
||||
// ValueListFilter (base class)
|
||||
ValueListFilter::ValueListFilter(std::initializer_list<TemplatableValue<float>> values) : values_(values) {}
|
||||
|
||||
bool ValueListFilter::value_matches_any_(float sensor_value) {
|
||||
int8_t accuracy = this->parent_->get_accuracy_decimals();
|
||||
// ValueListFilter helper (non-template, shared by all ValueListFilter<N> instantiations)
|
||||
bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue<float> *values, size_t count) {
|
||||
int8_t accuracy = parent->get_accuracy_decimals();
|
||||
float accuracy_mult = pow10_int(accuracy);
|
||||
float rounded_sensor = roundf(accuracy_mult * sensor_value);
|
||||
|
||||
for (auto &filter_value : this->values_) {
|
||||
float fv = filter_value.value();
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
float fv = values[i].value();
|
||||
|
||||
// Handle NaN comparison
|
||||
if (std::isnan(fv)) {
|
||||
@@ -248,16 +246,6 @@ bool ValueListFilter::value_matches_any_(float sensor_value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// FilterOutValueFilter
|
||||
FilterOutValueFilter::FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out)
|
||||
: ValueListFilter(values_to_filter_out) {}
|
||||
|
||||
optional<float> FilterOutValueFilter::new_value(float value) {
|
||||
if (this->value_matches_any_(value))
|
||||
return {}; // Filter out
|
||||
return value; // Pass through
|
||||
}
|
||||
|
||||
// ThrottleFilter
|
||||
ThrottleFilter::ThrottleFilter(uint32_t min_time_between_inputs) : min_time_between_inputs_(min_time_between_inputs) {}
|
||||
optional<float> ThrottleFilter::new_value(float value) {
|
||||
@@ -269,17 +257,13 @@ optional<float> ThrottleFilter::new_value(float value) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// ThrottleWithPriorityFilter
|
||||
ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(
|
||||
uint32_t min_time_between_inputs, std::initializer_list<TemplatableValue<float>> prioritized_values)
|
||||
: ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
|
||||
|
||||
optional<float> ThrottleWithPriorityFilter::new_value(float value) {
|
||||
// ThrottleWithPriorityFilter helper (non-template, keeps App access in .cpp)
|
||||
optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableValue<float> *values,
|
||||
size_t count, uint32_t &last_input, uint32_t min_time_between_inputs) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
// Allow value through if: no previous input, time expired, or is prioritized
|
||||
if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ ||
|
||||
this->value_matches_any_(value)) {
|
||||
this->last_input_ = now;
|
||||
if (last_input == 0 || now - last_input >= min_time_between_inputs ||
|
||||
value_list_matches_any(parent, value, values, count)) {
|
||||
last_input = now;
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_SENSOR_FILTER
|
||||
|
||||
#include <array>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "esphome/core/automation.h"
|
||||
@@ -328,28 +329,42 @@ class MultiplyFilter : public Filter {
|
||||
TemplatableValue<float> multiplier_;
|
||||
};
|
||||
|
||||
/** Base class for filters that compare sensor values against a list of configured values.
|
||||
/// Non-template helper for value matching (implementation in filter.cpp)
|
||||
bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue<float> *values, size_t count);
|
||||
|
||||
/** Base class for filters that compare sensor values against a fixed list of configured values.
|
||||
*
|
||||
* This base class provides common functionality for filters that need to check if a sensor
|
||||
* value matches any value in a configured list, with proper handling of NaN values and
|
||||
* accuracy-based rounding for comparisons.
|
||||
* Templated on N (the number of values) so the list is stored inline in a std::array,
|
||||
* avoiding heap allocation and the overhead of FixedVector.
|
||||
*
|
||||
* @tparam N Number of values in the filter list, set by code generation to match
|
||||
* the exact number of values configured in YAML.
|
||||
*/
|
||||
class ValueListFilter : public Filter {
|
||||
template<size_t N> class ValueListFilter : public Filter {
|
||||
protected:
|
||||
explicit ValueListFilter(std::initializer_list<TemplatableValue<float>> values);
|
||||
explicit ValueListFilter(std::initializer_list<TemplatableValue<float>> values) {
|
||||
init_array_from(this->values_, values);
|
||||
}
|
||||
|
||||
/// Check if sensor value matches any configured value (with accuracy rounding)
|
||||
bool value_matches_any_(float sensor_value);
|
||||
bool value_matches_any_(float sensor_value) {
|
||||
return value_list_matches_any(this->parent_, sensor_value, this->values_.data(), N);
|
||||
}
|
||||
|
||||
FixedVector<TemplatableValue<float>> values_;
|
||||
std::array<TemplatableValue<float>, N> values_{};
|
||||
};
|
||||
|
||||
/// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`.
|
||||
class FilterOutValueFilter : public ValueListFilter {
|
||||
template<size_t N> class FilterOutValueFilter : public ValueListFilter<N> {
|
||||
public:
|
||||
explicit FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out);
|
||||
explicit FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out)
|
||||
: ValueListFilter<N>(values_to_filter_out) {}
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
optional<float> new_value(float value) override {
|
||||
if (this->value_matches_any_(value))
|
||||
return {}; // Filter out
|
||||
return value; // Pass through
|
||||
}
|
||||
};
|
||||
|
||||
class ThrottleFilter : public Filter {
|
||||
@@ -363,13 +378,21 @@ class ThrottleFilter : public Filter {
|
||||
uint32_t min_time_between_inputs_;
|
||||
};
|
||||
|
||||
/// Non-template helper for ThrottleWithPriorityFilter (implementation in filter.cpp)
|
||||
optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableValue<float> *values,
|
||||
size_t count, uint32_t &last_input, uint32_t min_time_between_inputs);
|
||||
|
||||
/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`.
|
||||
class ThrottleWithPriorityFilter : public ValueListFilter {
|
||||
template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N> {
|
||||
public:
|
||||
explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
|
||||
std::initializer_list<TemplatableValue<float>> prioritized_values);
|
||||
std::initializer_list<TemplatableValue<float>> prioritized_values)
|
||||
: ValueListFilter<N>(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
optional<float> new_value(float value) override {
|
||||
return throttle_with_priority_new_value(this->parent_, value, this->values_.data(), N, this->last_input_,
|
||||
this->min_time_between_inputs_);
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t last_input_{0};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "version_text_sensor.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/build_info_data.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
@@ -36,7 +35,9 @@ void VersionTextSensor::setup() {
|
||||
if (!this->hide_timestamp_) {
|
||||
size_t len = strlen(version_str);
|
||||
ESPHOME_strncat_P(version_str, BUILT_STR, sizeof(version_str) - len - 1);
|
||||
ESPHOME_strncat_P(version_str, ESPHOME_BUILD_TIME_STR, sizeof(version_str) - strlen(version_str) - 1);
|
||||
char build_time_buf[Application::BUILD_TIME_STR_SIZE];
|
||||
App.get_build_time_string(build_time_buf);
|
||||
strncat(version_str, build_time_buf, sizeof(version_str) - strlen(version_str) - 1);
|
||||
}
|
||||
|
||||
// The closing parenthesis is part of the config-hash suffix and must
|
||||
|
||||
@@ -135,7 +135,7 @@ class WebServerBase {
|
||||
uint16_t get_port() const { return port_; }
|
||||
|
||||
protected:
|
||||
int initialized_{0};
|
||||
uint8_t initialized_{0};
|
||||
uint16_t port_{80};
|
||||
AsyncWebServer *server_{nullptr};
|
||||
std::vector<AsyncWebHandler *> handlers_;
|
||||
|
||||
@@ -84,6 +84,8 @@ void store_component_error_message(const Component *component, const char *messa
|
||||
|
||||
static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS =
|
||||
10U; ///< How long the blocking time must be larger to warn again
|
||||
// Threshold in ms (computed from centiseconds constant in component.h)
|
||||
static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast<uint32_t>(WARN_IF_BLOCKING_OVER_CS) * 10U;
|
||||
|
||||
float Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
@@ -268,15 +270,18 @@ void Component::call() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const LogString *Component::get_component_log_str() const {
|
||||
return component_source_lookup(this->component_source_index_);
|
||||
}
|
||||
bool Component::should_warn_of_blocking(uint32_t blocking_time) {
|
||||
if (blocking_time > this->warn_if_blocking_over_) {
|
||||
// Prevent overflow when adding increment - if we're about to overflow, just max out
|
||||
if (blocking_time + WARN_IF_BLOCKING_INCREMENT_MS < blocking_time ||
|
||||
blocking_time + WARN_IF_BLOCKING_INCREMENT_MS > std::numeric_limits<uint16_t>::max()) {
|
||||
this->warn_if_blocking_over_ = std::numeric_limits<uint16_t>::max();
|
||||
} else {
|
||||
this->warn_if_blocking_over_ = static_cast<uint16_t>(blocking_time + WARN_IF_BLOCKING_INCREMENT_MS);
|
||||
}
|
||||
// Convert centisecond threshold to milliseconds for comparison
|
||||
uint32_t threshold_ms = static_cast<uint32_t>(this->warn_if_blocking_over_) * 10U;
|
||||
if (blocking_time > threshold_ms) {
|
||||
// Set new threshold: blocking_time + increment, converted back to centiseconds
|
||||
uint32_t new_threshold_ms = blocking_time + WARN_IF_BLOCKING_INCREMENT_MS;
|
||||
uint32_t new_cs = new_threshold_ms / 10U;
|
||||
// Saturate at uint8_t max (255 = 2550ms)
|
||||
this->warn_if_blocking_over_ = static_cast<uint8_t>(new_cs > 255U ? 255U : new_cs);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -537,4 +542,7 @@ void clear_setup_priority_overrides() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Weak default for component_source_lookup - overridden by generated code
|
||||
__attribute__((weak)) const LogString *component_source_lookup(uint8_t) { return LOG_STR("<unknown>"); }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
+24
-13
@@ -11,6 +11,10 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/optional.h"
|
||||
|
||||
// Forward declarations for friend access from codegen-generated setup()
|
||||
void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h
|
||||
void original_setup(); // NOLINT(readability-redundant-declaration)
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// Forward declaration for LogString
|
||||
@@ -79,11 +83,14 @@ inline constexpr uint8_t STATUS_LED_WARNING = 0x08;
|
||||
inline constexpr uint8_t STATUS_LED_ERROR = 0x10;
|
||||
// Component loop override flag uses bit 5 (set at registration time)
|
||||
inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20;
|
||||
|
||||
// Remove before 2026.8.0
|
||||
enum class RetryResult { DONE, RETRY };
|
||||
|
||||
inline constexpr uint16_t WARN_IF_BLOCKING_OVER_MS = 50U;
|
||||
inline constexpr uint8_t WARN_IF_BLOCKING_OVER_CS = 5U; // 50ms in centiseconds (1cs = 10ms)
|
||||
|
||||
/// Lookup component source name by index (1-based). Generated by Python codegen.
|
||||
/// Weak default returns "<unknown>" so builds without codegen still link.
|
||||
const LogString *component_source_lookup(uint8_t index);
|
||||
|
||||
class Component {
|
||||
public:
|
||||
@@ -275,23 +282,25 @@ class Component {
|
||||
|
||||
bool has_overridden_loop() const { return (this->component_state_ & COMPONENT_HAS_LOOP) != 0; }
|
||||
|
||||
/** Set where this component was loaded from for some debug messages.
|
||||
*
|
||||
* This is set by the ESPHome core, and should not be called manually.
|
||||
*/
|
||||
void set_component_source(const LogString *source) { component_source_ = source; }
|
||||
/** Get the integration where this component was declared as a LogString for logging.
|
||||
*
|
||||
* Returns LOG_STR("<unknown>") if source not set
|
||||
*/
|
||||
const LogString *get_component_log_str() const {
|
||||
return this->component_source_ == nullptr ? LOG_STR("<unknown>") : this->component_source_;
|
||||
}
|
||||
const LogString *get_component_log_str() const;
|
||||
|
||||
bool should_warn_of_blocking(uint32_t blocking_time);
|
||||
|
||||
protected:
|
||||
friend class Application;
|
||||
friend void ::setup();
|
||||
friend void ::original_setup();
|
||||
|
||||
/** Set where this component was loaded from for some debug messages.
|
||||
*
|
||||
* This is set by the ESPHome core during setup, and should not be called manually.
|
||||
* @param index 1-based index into the component source lookup table (0 = not set)
|
||||
*/
|
||||
void set_component_source_(uint8_t index) { this->component_source_index_ = index; }
|
||||
|
||||
virtual void call_setup();
|
||||
void call_dump_config_();
|
||||
@@ -509,9 +518,9 @@ class Component {
|
||||
void status_clear_warning_slow_path_();
|
||||
void status_clear_error_slow_path_();
|
||||
|
||||
// Ordered for optimal packing on 32-bit systems
|
||||
const LogString *component_source_{nullptr};
|
||||
uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s)
|
||||
// Ordered for optimal packing on 32-bit systems (8 bytes total with vtable)
|
||||
uint8_t component_source_index_{0}; ///< Index into component source PROGMEM lookup table (0 = not set)
|
||||
uint8_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_CS}; ///< Warn threshold in centiseconds (max 2550ms)
|
||||
/// State of this component - each bit has a purpose:
|
||||
/// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE)
|
||||
/// Bit 3: STATUS_LED_WARNING
|
||||
@@ -588,6 +597,8 @@ class WarnIfComponentBlockingGuard {
|
||||
this->record_runtime_stats_();
|
||||
#endif
|
||||
#ifndef USE_BENCHMARK
|
||||
// Fast path: compare against constant threshold in ms (computed at compile time from centiseconds)
|
||||
static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast<uint32_t>(WARN_IF_BLOCKING_OVER_CS) * 10U;
|
||||
if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] {
|
||||
warn_blocking(this->component_, blocking_time);
|
||||
}
|
||||
|
||||
+61
-27
@@ -296,15 +296,36 @@ void log_entity_device_class(const char *tag, const char *prefix, const EntityBa
|
||||
#define LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj) log_entity_unit_of_measurement(tag, prefix, obj)
|
||||
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj);
|
||||
|
||||
/**
|
||||
* An entity that has a state.
|
||||
* @tparam T The type of the state
|
||||
/** Base class for entities that track a typed state value with change-detection and callbacks.
|
||||
*
|
||||
* This class does not store the state value — subclasses own their storage. Whether a state
|
||||
* has been set is tracked by EntityBase::has_state().
|
||||
*
|
||||
* Subclasses must implement:
|
||||
* - get_state(): return a const reference to the current value
|
||||
* - set_state_value(): store a new value (called only when the state actually changes)
|
||||
* - get_trigger_on_initial_state(): return whether callbacks should fire on the first state
|
||||
*
|
||||
* Subclasses may override set_new_state() to add behavior (logging, notifications) after calling
|
||||
* the base implementation. Since set_new_state() is virtual, callers like invalidate_state()
|
||||
* dispatch through the vtable to the subclass override in the .cpp, avoiding template code
|
||||
* bloat at inline call sites. Subclasses may also add a fast-path dedup check before calling
|
||||
* set_new_state() to skip virtual dispatch entirely when the state hasn't changed.
|
||||
*
|
||||
* Callback behavior:
|
||||
* - full_state_callbacks_: fired on every change, receives optional<T> previous and current
|
||||
* - state_callbacks_: fired only when the new state has a value, and either this is not the
|
||||
* first state (had_state) or trigger_on_initial_state is set
|
||||
*
|
||||
* @tparam T The type of the state value
|
||||
*/
|
||||
template<typename T> class StatefulEntityBase : public EntityBase {
|
||||
public:
|
||||
virtual bool has_state() const { return this->state_.has_value(); }
|
||||
virtual const T &get_state() const { return this->state_.value(); } // NOLINT(bugprone-unchecked-optional-access)
|
||||
virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); }
|
||||
/// Return the current state value. Only valid when has_state() is true.
|
||||
virtual const T &get_state() const = 0;
|
||||
/// Return the current state if available, otherwise return the provided default.
|
||||
T get_state_default(T default_value) const { return this->has_state() ? this->get_state() : default_value; }
|
||||
/// Clear the state — sets has_state() to false and fires callbacks with nullopt.
|
||||
void invalidate_state() { this->set_new_state({}); }
|
||||
|
||||
template<typename F> void add_full_state_callback(F &&callback) {
|
||||
@@ -314,33 +335,46 @@ template<typename T> class StatefulEntityBase : public EntityBase {
|
||||
this->state_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
void set_trigger_on_initial_state(bool trigger_on_initial_state) {
|
||||
this->trigger_on_initial_state_ = trigger_on_initial_state;
|
||||
}
|
||||
|
||||
protected:
|
||||
optional<T> state_{};
|
||||
/**
|
||||
* Set a new state for this entity. This will trigger callbacks only if the new state is different from the previous.
|
||||
/// Subclasses return whether callbacks should fire on the very first state.
|
||||
virtual bool get_trigger_on_initial_state() const = 0;
|
||||
|
||||
/** Apply a new state, de-duplicating and firing callbacks as needed.
|
||||
*
|
||||
* @param new_state The new state.
|
||||
* @return True if the state was changed, false if it was the same as before.
|
||||
* Pass nullopt to invalidate (clear) the state. Pass a value to set it.
|
||||
* Returns true if the state actually changed, false if it was the same.
|
||||
* Subclasses may override to add logging/notifications after calling the base.
|
||||
*/
|
||||
virtual bool set_new_state(const optional<T> &new_state) {
|
||||
if (this->state_ != new_state) {
|
||||
// call the full state callbacks with the previous and new state
|
||||
this->full_state_callbacks_.call(this->state_, new_state);
|
||||
// trigger legacy callbacks only if the new state is valid and either the trigger on initial state is enabled or
|
||||
// the previous state was valid
|
||||
auto had_state = this->has_state();
|
||||
this->state_ = new_state;
|
||||
if (new_state.has_value() && (this->trigger_on_initial_state_ || had_state))
|
||||
this->state_callbacks_.call(new_state.value());
|
||||
return true;
|
||||
// Access flags_ directly to avoid function call overhead in this hot path
|
||||
bool had_state = this->flags_.has_state;
|
||||
// Use pointer to avoid requiring T to be default-constructible
|
||||
const T *current = had_state ? &this->get_state() : nullptr;
|
||||
if (new_state.has_value()) {
|
||||
if (current != nullptr && *current == new_state.value())
|
||||
return false; // same value, no change
|
||||
} else if (!had_state) {
|
||||
return false; // already invalidated, no change
|
||||
}
|
||||
return false;
|
||||
// Capture old_state before set_state_value — current pointer aliases subclass storage
|
||||
bool has_full_cbs = !this->full_state_callbacks_.empty();
|
||||
optional<T> old_state;
|
||||
if (has_full_cbs)
|
||||
old_state = current != nullptr ? optional<T>(*current) : nullopt;
|
||||
// Update storage before firing callbacks so callback code can inspect current state
|
||||
this->flags_.has_state = new_state.has_value();
|
||||
if (new_state.has_value()) {
|
||||
this->set_state_value(new_state.value());
|
||||
}
|
||||
if (has_full_cbs)
|
||||
this->full_state_callbacks_.call(old_state, new_state);
|
||||
// had_state first: on every change except the first, skips the virtual call
|
||||
if (new_state.has_value() && (had_state || this->get_trigger_on_initial_state()))
|
||||
this->state_callbacks_.call(new_state.value());
|
||||
return true;
|
||||
}
|
||||
bool trigger_on_initial_state_{true};
|
||||
/// Subclasses implement this to store the actual value into their own storage.
|
||||
virtual void set_state_value(const T &value) = 0;
|
||||
LazyCallbackManager<void(optional<T> previous, optional<T> current)> full_state_callbacks_;
|
||||
LazyCallbackManager<void(T)> state_callbacks_;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
@@ -497,6 +498,23 @@ template<typename T, size_t MAX_CAPACITY = std::numeric_limits<uint16_t>::max()>
|
||||
index_type capacity_{0};
|
||||
};
|
||||
|
||||
/// Initialize a std::array from an initializer_list. Uses memcpy for trivially copyable types (optimal codegen),
|
||||
/// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue).
|
||||
/// N is set by code generation; assert catches mismatches in debug/integration tests.
|
||||
template<typename T, size_t N> inline void init_array_from(std::array<T, N> &dest, std::initializer_list<T> src) {
|
||||
#ifdef ESPHOME_DEBUG
|
||||
assert(src.size() == N);
|
||||
#endif
|
||||
if constexpr (std::is_trivially_copyable_v<T>) {
|
||||
__builtin_memcpy(dest.data(), src.begin(), N * sizeof(T));
|
||||
} else {
|
||||
size_t i = 0;
|
||||
for (const auto &v : src) {
|
||||
dest[i++] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fixed-capacity vector - allocates once at runtime, never reallocates
|
||||
/// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append)
|
||||
/// when size is known at initialization but not at compile time
|
||||
|
||||
+120
-3
@@ -1,3 +1,4 @@
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
|
||||
from esphome.const import (
|
||||
@@ -7,15 +8,130 @@ from esphome.const import (
|
||||
CONF_UPDATE_INTERVAL,
|
||||
KEY_PAST_SAFE_MODE,
|
||||
)
|
||||
from esphome.core import CORE, ID, coroutine
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine, coroutine_with_priority
|
||||
from esphome.coroutine import FakeAwaitable
|
||||
from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable
|
||||
from esphome.cpp_generator import (
|
||||
RawStatement,
|
||||
add,
|
||||
add_define,
|
||||
add_global,
|
||||
get_variable,
|
||||
)
|
||||
from esphome.cpp_types import App
|
||||
from esphome.helpers import cpp_string_escape
|
||||
from esphome.types import ConfigFragmentType, ConfigType
|
||||
from esphome.util import Registry, RegistryEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_COMPONENT_SOURCE_DOMAIN = "component_source_pool"
|
||||
|
||||
# Maximum unique component source names (8-bit index, 0 = not set)
|
||||
_MAX_COMPONENT_SOURCES = 0xFF # 255
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComponentSourcePool:
|
||||
"""Pool of component source names for PROGMEM lookup table.
|
||||
|
||||
Source names are registered during to_code() and assigned 1-based indices.
|
||||
Index 0 means "not set" (returns LOG_STR("<unknown>")). At render time,
|
||||
the pool generates a C++ PROGMEM table + lookup function.
|
||||
"""
|
||||
|
||||
sources: dict[str, int] = field(default_factory=dict)
|
||||
table_registered: bool = False
|
||||
|
||||
|
||||
def _get_source_pool() -> ComponentSourcePool:
|
||||
"""Get or create the component source pool from CORE.data."""
|
||||
if _COMPONENT_SOURCE_DOMAIN not in CORE.data:
|
||||
CORE.data[_COMPONENT_SOURCE_DOMAIN] = ComponentSourcePool()
|
||||
return CORE.data[_COMPONENT_SOURCE_DOMAIN]
|
||||
|
||||
|
||||
def _ensure_source_table_registered() -> None:
|
||||
"""Schedule the table generation job (once)."""
|
||||
pool = _get_source_pool()
|
||||
if pool.table_registered:
|
||||
return
|
||||
pool.table_registered = True
|
||||
CORE.add_job(_generate_component_source_table)
|
||||
|
||||
|
||||
def register_component_source(name: str) -> int:
|
||||
"""Register a component source name and return its 1-based index.
|
||||
|
||||
Deduplicates: multiple components from the same source share one index.
|
||||
"""
|
||||
if not name:
|
||||
return 0
|
||||
pool = _get_source_pool()
|
||||
if name in pool.sources:
|
||||
return pool.sources[name]
|
||||
idx = len(pool.sources) + 1
|
||||
if idx > _MAX_COMPONENT_SOURCES:
|
||||
_LOGGER.warning(
|
||||
"Too many unique component source names (max %d), '%s' will show as '<unknown>'",
|
||||
_MAX_COMPONENT_SOURCES,
|
||||
name,
|
||||
)
|
||||
return 0
|
||||
pool.sources[name] = idx
|
||||
_ensure_source_table_registered()
|
||||
return idx
|
||||
|
||||
|
||||
def _generate_source_table_code(
|
||||
table_var: str,
|
||||
lookup_fn: str,
|
||||
strings: dict[str, int],
|
||||
) -> str:
|
||||
"""Generate C++ PROGMEM table + LogString* lookup for component sources.
|
||||
|
||||
Same pattern as entity_helpers._generate_category_code but returns
|
||||
const LogString* instead of const char* (needed for LOG_STR_ARG).
|
||||
"""
|
||||
if not strings:
|
||||
return ""
|
||||
|
||||
sorted_strings = sorted(strings.items(), key=lambda x: x[1])
|
||||
count = len(sorted_strings)
|
||||
|
||||
# Emit individual PROGMEM char arrays so string data lives in flash on ESP8266
|
||||
lines: list[str] = []
|
||||
var_names: list[str] = []
|
||||
for i, (s, _) in enumerate(sorted_strings):
|
||||
var_name = f"{table_var}_STR_{i}"
|
||||
var_names.append(var_name)
|
||||
lines.append(
|
||||
f"static const char {var_name}[] PROGMEM = {cpp_string_escape(s)};"
|
||||
)
|
||||
|
||||
entries = ", ".join(var_names)
|
||||
lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};")
|
||||
lines.append(f"const LogString *{lookup_fn}(uint8_t index) {{")
|
||||
lines.append(f' if (index == 0 || index > {count}) return LOG_STR("<unknown>");')
|
||||
lines.append(" return reinterpret_cast<const LogString *>(")
|
||||
lines.append(f" progmem_read_ptr(&{table_var}[index - 1]));")
|
||||
lines.append("}")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _generate_component_source_table() -> None:
|
||||
"""Generate the component source lookup table as a FINAL-priority job.
|
||||
|
||||
Runs after all component to_code() calls have registered their sources.
|
||||
"""
|
||||
pool = _get_source_pool()
|
||||
if code := _generate_source_table_code(
|
||||
"COMP_SRC_TABLE", "component_source_lookup", pool.sources
|
||||
):
|
||||
add_global(
|
||||
RawStatement(f"namespace esphome {{\n{code}}} // namespace esphome")
|
||||
)
|
||||
|
||||
|
||||
async def gpio_pin_expression(conf):
|
||||
"""Generate an expression for the given pin option.
|
||||
@@ -77,7 +193,8 @@ async def register_component(var, config):
|
||||
"Error while finding name of component, please report this", exc_info=e
|
||||
)
|
||||
if name is not None:
|
||||
add(var.set_component_source(LogStringLiteral(name)))
|
||||
idx = register_component_source(name)
|
||||
add(var.set_component_source_(idx))
|
||||
|
||||
add(App.register_component_(var))
|
||||
|
||||
|
||||
@@ -14,3 +14,6 @@ esphome:
|
||||
assert(x == 95);
|
||||
x = clamp_at_most(x, 40);
|
||||
assert(x == 40);
|
||||
- lambda: |-
|
||||
float value = 0.0f;
|
||||
sscanf("3.14", "%f", &value);
|
||||
|
||||
@@ -73,9 +73,16 @@ async def test_uart_mock_ld2410(
|
||||
],
|
||||
)
|
||||
|
||||
# Signal when we see recovery frame values
|
||||
# Signal when we see ALL recovery frame values to avoid race where some
|
||||
# arrive after the waiter fires but before we index into the lists
|
||||
recovery_received = collector.add_waiter(
|
||||
lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"]
|
||||
lambda: (
|
||||
pytest.approx(50.0) in collector.sensor_states["moving_distance"]
|
||||
and pytest.approx(75.0) in collector.sensor_states["still_distance"]
|
||||
and pytest.approx(100.0) in collector.sensor_states["moving_energy"]
|
||||
and pytest.approx(80.0) in collector.sensor_states["still_energy"]
|
||||
and pytest.approx(127.0) in collector.sensor_states["detection_distance"]
|
||||
)
|
||||
)
|
||||
|
||||
async with (
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Tests for ESP8266 component."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp8266 import lambdas_use_scanf_float
|
||||
from esphome.core import Lambda
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("src", "expected"),
|
||||
[
|
||||
# Basic float formats
|
||||
('sscanf(buf, "%f", &v)', True),
|
||||
('sscanf(buf, "%F", &v)', True),
|
||||
('sscanf(buf, "%e", &v)', True),
|
||||
('sscanf(buf, "%E", &v)', True),
|
||||
('sscanf(buf, "%g", &v)', True),
|
||||
('sscanf(buf, "%G", &v)', True),
|
||||
('sscanf(buf, "%a", &v)', True),
|
||||
('sscanf(buf, "%A", &v)', True),
|
||||
# With modifiers
|
||||
('sscanf(buf, "%lf", &v)', True),
|
||||
('sscanf(buf, "%Lf", &v)', True),
|
||||
('sscanf(buf, "%8lf", &v)', True),
|
||||
('sscanf(buf, "%*f")', True),
|
||||
('sscanf(buf, "%.2f", &v)', True),
|
||||
# Mixed formats
|
||||
('sscanf(buf, "%d,%f", &a, &b)', True),
|
||||
# fscanf and std::sscanf
|
||||
('fscanf(fp, "%f", &v)', True),
|
||||
('std::sscanf(buf, "%f", &v)', True),
|
||||
# Multi-line
|
||||
('sscanf(buf,\n"%f", &v)', True),
|
||||
# No float format
|
||||
('sscanf(buf, "%d", &v)', False),
|
||||
('sscanf(buf, "%s", s)', False),
|
||||
# printf not scanf
|
||||
('printf("%f", val)', False),
|
||||
# %f in a different statement after scanf
|
||||
('sscanf(buf, "%d", &x); printf("%f", val);', False),
|
||||
# scanf %f in comment only
|
||||
('// sscanf(buf, "%f", &v)\nsscanf(buf, "%d", &x)', False),
|
||||
('/* sscanf(buf, "%f") */\nsscanf(buf, "%d", &x)', False),
|
||||
],
|
||||
)
|
||||
def test_lambdas_use_scanf_float(src: str, expected: bool) -> None:
|
||||
"""Test scanf float detection in lambda source."""
|
||||
config: ConfigType = {"test": [Lambda(src)]}
|
||||
assert lambdas_use_scanf_float(config) is expected
|
||||
|
||||
|
||||
def test_lambdas_use_scanf_float_no_lambdas() -> None:
|
||||
"""Test with config containing no lambdas."""
|
||||
config: ConfigType = {"key": "value", "list": [1, 2]}
|
||||
assert lambdas_use_scanf_float(config) is False
|
||||
|
||||
|
||||
def test_lambdas_use_scanf_float_nested() -> None:
|
||||
"""Test detection in deeply nested config."""
|
||||
config: ConfigType = {"a": {"b": {"c": [Lambda('sscanf(buf, "%f", &v)')]}}}
|
||||
assert lambdas_use_scanf_float(config) is True
|
||||
@@ -1,8 +1,10 @@
|
||||
import logging
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import const, cpp_helpers as ch
|
||||
from esphome.cpp_helpers import ComponentSourcePool, register_component_source
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -23,7 +25,7 @@ async def test_register_component(monkeypatch):
|
||||
app_mock = Mock(register_component_=Mock(return_value=var))
|
||||
monkeypatch.setattr(ch, "App", app_mock)
|
||||
|
||||
core_mock = Mock(component_ids=["foo.bar"])
|
||||
core_mock = Mock(component_ids=["foo.bar"], data={})
|
||||
monkeypatch.setattr(ch, "CORE", core_mock)
|
||||
|
||||
add_mock = Mock()
|
||||
@@ -59,7 +61,7 @@ async def test_register_component__with_setup_priority(monkeypatch):
|
||||
app_mock = Mock(register_component_=Mock(return_value=var))
|
||||
monkeypatch.setattr(ch, "App", app_mock)
|
||||
|
||||
core_mock = Mock(component_ids=["foo.bar"])
|
||||
core_mock = Mock(component_ids=["foo.bar"], data={})
|
||||
monkeypatch.setattr(ch, "CORE", core_mock)
|
||||
|
||||
add_mock = Mock()
|
||||
@@ -78,3 +80,69 @@ async def test_register_component__with_setup_priority(monkeypatch):
|
||||
assert add_mock.call_count == 4
|
||||
app_mock.register_component_.assert_called_with(var)
|
||||
assert core_mock.component_ids == []
|
||||
|
||||
|
||||
def test_register_component_source_empty_name(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(ch, "CORE", Mock(data={}))
|
||||
assert register_component_source("") == 0
|
||||
|
||||
|
||||
def test_register_component_source_deduplicates(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(ch, "CORE", Mock(data={}))
|
||||
idx1 = register_component_source("wifi")
|
||||
idx2 = register_component_source("api")
|
||||
idx3 = register_component_source("wifi")
|
||||
assert idx1 == 1
|
||||
assert idx2 == 2
|
||||
assert idx3 == 1 # deduplicated
|
||||
|
||||
|
||||
def test_generate_source_table_code_empty() -> None:
|
||||
from esphome.cpp_helpers import _generate_source_table_code
|
||||
|
||||
assert _generate_source_table_code("TBL", "lookup", {}) == ""
|
||||
|
||||
|
||||
def test_generate_source_table_code_non_empty() -> None:
|
||||
from esphome.cpp_helpers import _generate_source_table_code
|
||||
|
||||
code = _generate_source_table_code("TBL", "lookup", {"wifi": 1, "api": 2})
|
||||
assert "PROGMEM" in code
|
||||
assert "wifi" in code
|
||||
assert "api" in code
|
||||
assert "lookup" in code
|
||||
assert "index == 0" in code
|
||||
assert "progmem_read_ptr" in code
|
||||
assert "index > 2" in code
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_component_source_table_empty_pool(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test that _generate_component_source_table does nothing with an empty pool."""
|
||||
from esphome.cpp_helpers import _generate_component_source_table
|
||||
|
||||
monkeypatch.setattr(ch, "CORE", Mock(data={}))
|
||||
add_global_mock = Mock()
|
||||
monkeypatch.setattr(ch, "add_global", add_global_mock)
|
||||
await _generate_component_source_table()
|
||||
add_global_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_register_component_source_overflow_warns(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
# Pre-fill pool to max
|
||||
pool = ComponentSourcePool(
|
||||
sources={f"comp_{i}": i + 1 for i in range(0xFF)},
|
||||
table_registered=True,
|
||||
)
|
||||
monkeypatch.setattr(ch, "CORE", Mock(data={ch._COMPONENT_SOURCE_DOMAIN: pool}))
|
||||
with caplog.at_level(logging.WARNING):
|
||||
idx = register_component_source("overflow_component")
|
||||
assert idx == 0
|
||||
assert "Too many unique component source names" in caplog.text
|
||||
assert "overflow_component" in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user