mirror of
https://github.com/esphome/esphome.git
synced 2025-11-16 06:45:48 +00:00
Compare commits
11 Commits
dev
...
de_dupe_la
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6378990cd | ||
|
|
e9ff4d3c4e | ||
|
|
4081345013 | ||
|
|
5989b78e93 | ||
|
|
5727043cec | ||
|
|
1441c7fab2 | ||
|
|
62248b6bba | ||
|
|
b7c105125e | ||
|
|
11de948698 | ||
|
|
6ade327cde | ||
|
|
cc1b547ad2 |
@@ -1,11 +1,12 @@
|
||||
#include "automation.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
namespace esphome {
|
||||
namespace binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor.automation";
|
||||
|
||||
void MultiClickTrigger::on_state_(bool state) {
|
||||
void binary_sensor::MultiClickTrigger::on_state_(bool state) {
|
||||
// Handle duplicate events
|
||||
if (state == this->last_state_) {
|
||||
return;
|
||||
@@ -66,7 +67,7 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
|
||||
*this->at_index_ = *this->at_index_ + 1;
|
||||
}
|
||||
void MultiClickTrigger::schedule_cooldown_() {
|
||||
void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
||||
this->is_in_cooldown_ = true;
|
||||
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
|
||||
@@ -78,7 +79,7 @@ void MultiClickTrigger::schedule_cooldown_() {
|
||||
this->cancel_timeout("is_valid");
|
||||
this->cancel_timeout("is_not_valid");
|
||||
}
|
||||
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
if (min_length == 0) {
|
||||
this->is_valid_ = true;
|
||||
return;
|
||||
@@ -89,19 +90,19 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
this->is_valid_ = true;
|
||||
});
|
||||
}
|
||||
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
this->set_timeout("is_not_valid", max_length, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
});
|
||||
}
|
||||
void MultiClickTrigger::cancel() {
|
||||
void binary_sensor::MultiClickTrigger::cancel() {
|
||||
ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled.");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
}
|
||||
void MultiClickTrigger::trigger_() {
|
||||
void binary_sensor::MultiClickTrigger::trigger_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
|
||||
this->at_index_.reset();
|
||||
this->cancel_timeout("trigger");
|
||||
@@ -117,4 +118,5 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
|
||||
return length >= min_length && length <= max_length;
|
||||
}
|
||||
}
|
||||
} // namespace esphome::binary_sensor
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
namespace esphome {
|
||||
namespace binary_sensor {
|
||||
|
||||
struct MultiClickTriggerEvent {
|
||||
bool state;
|
||||
@@ -171,4 +172,5 @@ template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts..
|
||||
BinarySensor *sensor_;
|
||||
};
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor";
|
||||
|
||||
@@ -61,4 +63,6 @@ void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
|
||||
}
|
||||
bool BinarySensor::is_status_binary_sensor() const { return false; }
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
|
||||
#include <initializer_list>
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
|
||||
class BinarySensor;
|
||||
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj);
|
||||
@@ -68,4 +70,5 @@ class BinarySensorInitiallyOff : public BinarySensor {
|
||||
bool has_state() const override { return true; }
|
||||
};
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
#include "binary_sensor.h"
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
|
||||
static const char *const TAG = "sensor.filter";
|
||||
|
||||
@@ -130,4 +132,6 @@ optional<bool> SettleFilter::new_value(bool value) {
|
||||
|
||||
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
|
||||
class BinarySensor;
|
||||
|
||||
@@ -137,4 +139,6 @@ class SettleFilter : public Filter, public Component {
|
||||
bool steady_{true};
|
||||
};
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -36,6 +36,7 @@ from esphome.const import (
|
||||
CONF_WEIGHT,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.helpers import cpp_string_escape
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -49,6 +50,7 @@ font_ns = cg.esphome_ns.namespace("font")
|
||||
|
||||
Font = font_ns.class_("Font")
|
||||
Glyph = font_ns.class_("Glyph")
|
||||
GlyphData = font_ns.struct("GlyphData")
|
||||
|
||||
CONF_BPP = "bpp"
|
||||
CONF_EXTRAS = "extras"
|
||||
@@ -461,7 +463,7 @@ FONT_SCHEMA = cv.Schema(
|
||||
)
|
||||
),
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(Glyph),
|
||||
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -581,15 +583,22 @@ async def to_code(config):
|
||||
|
||||
# Create the glyph table that points to data in the above array.
|
||||
glyph_initializer = [
|
||||
[
|
||||
x.glyph,
|
||||
prog_arr + (y - len(x.bitmap_data)),
|
||||
x.advance,
|
||||
x.offset_x,
|
||||
x.offset_y,
|
||||
x.width,
|
||||
x.height,
|
||||
]
|
||||
cg.StructInitializer(
|
||||
GlyphData,
|
||||
(
|
||||
"a_char",
|
||||
cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"),
|
||||
),
|
||||
(
|
||||
"data",
|
||||
cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"),
|
||||
),
|
||||
("advance", x.advance),
|
||||
("offset_x", x.offset_x),
|
||||
("offset_y", x.offset_y),
|
||||
("width", x.width),
|
||||
("height", x.height),
|
||||
)
|
||||
for (x, y) in zip(
|
||||
glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args]))
|
||||
)
|
||||
|
||||
@@ -9,19 +9,20 @@ namespace font {
|
||||
|
||||
static const char *const TAG = "font";
|
||||
|
||||
const uint8_t *Glyph::get_char() const { return this->glyph_data_->a_char; }
|
||||
// Compare the char at the string position with this char.
|
||||
// Return true if this char is less than or equal the other.
|
||||
bool Glyph::compare_to(const uint8_t *str) const {
|
||||
// 1 -> this->char_
|
||||
// 2 -> str
|
||||
for (uint32_t i = 0;; i++) {
|
||||
if (this->a_char[i] == '\0')
|
||||
if (this->glyph_data_->a_char[i] == '\0')
|
||||
return true;
|
||||
if (str[i] == '\0')
|
||||
return false;
|
||||
if (this->a_char[i] > str[i])
|
||||
if (this->glyph_data_->a_char[i] > str[i])
|
||||
return false;
|
||||
if (this->a_char[i] < str[i])
|
||||
if (this->glyph_data_->a_char[i] < str[i])
|
||||
return true;
|
||||
}
|
||||
// this should not happen
|
||||
@@ -29,32 +30,35 @@ bool Glyph::compare_to(const uint8_t *str) const {
|
||||
}
|
||||
int Glyph::match_length(const uint8_t *str) const {
|
||||
for (uint32_t i = 0;; i++) {
|
||||
if (this->a_char[i] == '\0')
|
||||
if (this->glyph_data_->a_char[i] == '\0')
|
||||
return i;
|
||||
if (str[i] != this->a_char[i])
|
||||
if (str[i] != this->glyph_data_->a_char[i])
|
||||
return 0;
|
||||
}
|
||||
// this should not happen
|
||||
return 0;
|
||||
}
|
||||
void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
|
||||
*x1 = this->offset_x;
|
||||
*y1 = this->offset_y;
|
||||
*width = this->width;
|
||||
*height = this->height;
|
||||
*x1 = this->glyph_data_->offset_x;
|
||||
*y1 = this->glyph_data_->offset_y;
|
||||
*width = this->glyph_data_->width;
|
||||
*height = this->glyph_data_->height;
|
||||
}
|
||||
|
||||
Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
uint8_t bpp)
|
||||
: glyphs_(ConstVector(data, data_nr)),
|
||||
baseline_(baseline),
|
||||
: baseline_(baseline),
|
||||
height_(height),
|
||||
descender_(descender),
|
||||
linegap_(height - baseline - descender),
|
||||
xheight_(xheight),
|
||||
capheight_(capheight),
|
||||
bpp_(bpp) {}
|
||||
int Font::match_next_glyph(const uint8_t *str, int *match_length) const {
|
||||
bpp_(bpp) {
|
||||
glyphs_.reserve(data_nr);
|
||||
for (int i = 0; i < data_nr; ++i)
|
||||
glyphs_.emplace_back(&data[i]);
|
||||
}
|
||||
int Font::match_next_glyph(const uint8_t *str, int *match_length) {
|
||||
int lo = 0;
|
||||
int hi = this->glyphs_.size() - 1;
|
||||
while (lo != hi) {
|
||||
@@ -84,18 +88,18 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
|
||||
if (glyph_n < 0) {
|
||||
// Unknown char, skip
|
||||
if (!this->get_glyphs().empty())
|
||||
x += this->get_glyphs()[0].advance;
|
||||
x += this->get_glyphs()[0].glyph_data_->advance;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const Glyph &glyph = this->glyphs_[glyph_n];
|
||||
if (!has_char) {
|
||||
min_x = glyph.offset_x;
|
||||
min_x = glyph.glyph_data_->offset_x;
|
||||
} else {
|
||||
min_x = std::min(min_x, x + glyph.offset_x);
|
||||
min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
|
||||
}
|
||||
x += glyph.advance;
|
||||
x += glyph.glyph_data_->advance;
|
||||
|
||||
i += match_length;
|
||||
has_char = true;
|
||||
@@ -114,7 +118,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
|
||||
// Unknown char, skip
|
||||
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
|
||||
if (!this->get_glyphs().empty()) {
|
||||
uint8_t glyph_width = this->get_glyphs()[0].advance;
|
||||
uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->advance;
|
||||
display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color);
|
||||
x_at += glyph_width;
|
||||
}
|
||||
@@ -126,7 +130,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
|
||||
const Glyph &glyph = this->get_glyphs()[glyph_n];
|
||||
glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
|
||||
|
||||
const uint8_t *data = glyph.data;
|
||||
const uint8_t *data = glyph.glyph_data_->data;
|
||||
const int max_x = x_at + scan_x1 + scan_width;
|
||||
const int max_y = y_start + scan_y1 + scan_height;
|
||||
|
||||
@@ -164,7 +168,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
|
||||
}
|
||||
}
|
||||
}
|
||||
x_at += glyph.advance;
|
||||
x_at += glyph.glyph_data_->advance;
|
||||
|
||||
i += match_length;
|
||||
}
|
||||
|
||||
@@ -12,19 +12,21 @@ namespace font {
|
||||
|
||||
class Font;
|
||||
|
||||
struct GlyphData {
|
||||
const uint8_t *a_char;
|
||||
const uint8_t *data;
|
||||
int advance;
|
||||
int offset_x;
|
||||
int offset_y;
|
||||
int width;
|
||||
int height;
|
||||
};
|
||||
|
||||
class Glyph {
|
||||
public:
|
||||
constexpr Glyph(const char *a_char, const uint8_t *data, int advance, int offset_x, int offset_y, int width,
|
||||
int height)
|
||||
: a_char(a_char),
|
||||
data(data),
|
||||
advance(advance),
|
||||
offset_x(offset_x),
|
||||
offset_y(offset_y),
|
||||
width(width),
|
||||
height(height) {}
|
||||
Glyph(const GlyphData *data) : glyph_data_(data) {}
|
||||
|
||||
const uint8_t *get_char() const { return reinterpret_cast<const uint8_t *>(this->a_char); }
|
||||
const uint8_t *get_char() const;
|
||||
|
||||
bool compare_to(const uint8_t *str) const;
|
||||
|
||||
@@ -32,16 +34,12 @@ class Glyph {
|
||||
|
||||
void scan_area(int *x1, int *y1, int *width, int *height) const;
|
||||
|
||||
const char *a_char;
|
||||
const uint8_t *data;
|
||||
int advance;
|
||||
int offset_x;
|
||||
int offset_y;
|
||||
int width;
|
||||
int height;
|
||||
const GlyphData *get_glyph_data() const { return this->glyph_data_; }
|
||||
|
||||
protected:
|
||||
friend Font;
|
||||
|
||||
const GlyphData *glyph_data_;
|
||||
};
|
||||
|
||||
class Font
|
||||
@@ -52,8 +50,8 @@ class Font
|
||||
public:
|
||||
/** Construct the font with the given glyphs.
|
||||
*
|
||||
* @param data A list of glyphs, must be sorted lexicographically.
|
||||
* @param data_nr The number of glyphs
|
||||
* @param data A vector of glyphs, must be sorted lexicographically.
|
||||
* @param data_nr The number of glyphs in data.
|
||||
* @param baseline The y-offset from the top of the text to the baseline.
|
||||
* @param height The y-offset from the top of the text to the bottom.
|
||||
* @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p).
|
||||
@@ -61,10 +59,10 @@ class Font
|
||||
* @param capheight The height of capital letters, usually measured at the "X" glyph.
|
||||
* @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps.
|
||||
*/
|
||||
Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
|
||||
uint8_t bpp = 1);
|
||||
|
||||
int match_next_glyph(const uint8_t *str, int *match_length) const;
|
||||
int match_next_glyph(const uint8_t *str, int *match_length);
|
||||
|
||||
#ifdef USE_DISPLAY
|
||||
void print(int x_start, int y_start, display::Display *display, Color color, const char *text,
|
||||
@@ -80,10 +78,10 @@ class Font
|
||||
inline int get_capheight() { return this->capheight_; }
|
||||
inline int get_bpp() { return this->bpp_; }
|
||||
|
||||
const ConstVector<Glyph> &get_glyphs() const { return glyphs_; }
|
||||
const std::vector<Glyph, RAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
|
||||
|
||||
protected:
|
||||
ConstVector<Glyph> glyphs_;
|
||||
std::vector<Glyph, RAMAllocator<Glyph>> glyphs_;
|
||||
int baseline_;
|
||||
int height_;
|
||||
int descender_;
|
||||
|
||||
@@ -31,83 +31,35 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
|
||||
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_LIGHT): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_LIGHTBULB,
|
||||
),
|
||||
cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
@@ -121,13 +73,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
|
||||
],
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
@@ -135,13 +81,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
|
||||
],
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
|
||||
@@ -31,84 +31,36 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
|
||||
cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_LIGHT): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_LIGHTBULB,
|
||||
unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor
|
||||
),
|
||||
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_SIGNAL,
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
],
|
||||
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
@@ -122,13 +74,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
|
||||
],
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
@@ -136,13 +82,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=[
|
||||
{
|
||||
"timeout": {
|
||||
"timeout": cv.TimePeriod(milliseconds=1000),
|
||||
"value": "last",
|
||||
}
|
||||
},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
|
||||
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
|
||||
],
|
||||
icon=ICON_FLASH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
|
||||
@@ -43,7 +43,7 @@ FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) {
|
||||
|
||||
const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; }
|
||||
|
||||
const font::Glyph *FontEngine::get_glyph_data(uint32_t unicode_letter) {
|
||||
const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) {
|
||||
if (unicode_letter == last_letter_)
|
||||
return this->last_data_;
|
||||
uint8_t unicode[5];
|
||||
@@ -67,7 +67,7 @@ const font::Glyph *FontEngine::get_glyph_data(uint32_t unicode_letter) {
|
||||
int glyph_n = this->font_->match_next_glyph(unicode, &match_length);
|
||||
if (glyph_n < 0)
|
||||
return nullptr;
|
||||
this->last_data_ = &this->font_->get_glyphs()[glyph_n];
|
||||
this->last_data_ = this->font_->get_glyphs()[glyph_n].get_glyph_data();
|
||||
this->last_letter_ = unicode_letter;
|
||||
return this->last_data_;
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ class FontEngine {
|
||||
FontEngine(font::Font *esp_font);
|
||||
const lv_font_t *get_lv_font();
|
||||
|
||||
const font::Glyph *get_glyph_data(uint32_t unicode_letter);
|
||||
const font::GlyphData *get_glyph_data(uint32_t unicode_letter);
|
||||
uint16_t baseline{};
|
||||
uint16_t height{};
|
||||
uint8_t bpp{};
|
||||
@@ -148,7 +148,7 @@ class FontEngine {
|
||||
protected:
|
||||
font::Font *font_{};
|
||||
uint32_t last_letter_{};
|
||||
const font::Glyph *last_data_{};
|
||||
const font::GlyphData *last_data_{};
|
||||
lv_font_t lv_font_{};
|
||||
};
|
||||
#endif // USE_LVGL_FONT
|
||||
|
||||
@@ -111,23 +111,6 @@ template<> constexpr int64_t byteswap(int64_t n) { return __builtin_bswap64(n);
|
||||
/// @name Container utilities
|
||||
///@{
|
||||
|
||||
/// Lightweight read-only view over a const array stored in RODATA (will typically be in flash memory)
|
||||
/// Avoids copying data from flash to RAM by keeping a pointer to the flash data.
|
||||
/// Similar to std::span but with minimal overhead for embedded systems.
|
||||
|
||||
template<typename T> class ConstVector {
|
||||
public:
|
||||
constexpr ConstVector(const T *data, size_t size) : data_(data), size_(size) {}
|
||||
|
||||
const constexpr T &operator[](size_t i) const { return data_[i]; }
|
||||
constexpr size_t size() const { return size_; }
|
||||
constexpr bool empty() const { return size_ == 0; }
|
||||
|
||||
protected:
|
||||
const T *data_;
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
/// Minimal static vector - saves memory by avoiding std::vector overhead
|
||||
template<typename T, size_t N> class StaticVector {
|
||||
public:
|
||||
|
||||
@@ -609,12 +609,13 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
|
||||
if (now < last && (last - now) > HALF_MAX_UINT32) {
|
||||
this->millis_major_++;
|
||||
major++;
|
||||
this->last_millis_ = now;
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
|
||||
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||
} else if (now > last) {
|
||||
// Only update if time moved forward
|
||||
}
|
||||
|
||||
// Only update if time moved forward
|
||||
if (now > last) {
|
||||
this->last_millis_ = now;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,21 @@ from esphome.core import (
|
||||
TimePeriodNanoseconds,
|
||||
TimePeriodSeconds,
|
||||
)
|
||||
from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||
from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last
|
||||
from esphome.types import Expression, SafeExpType, TemplateArgsType
|
||||
from esphome.util import OrderedDict
|
||||
from esphome.yaml_util import ESPHomeDataBase
|
||||
|
||||
# Keys for lambda deduplication storage in CORE.data
|
||||
_KEY_LAMBDA_DEDUP = "lambda_dedup"
|
||||
_KEY_LAMBDA_DEDUP_DECLARATIONS = "lambda_dedup_declarations"
|
||||
|
||||
# Regex patterns for static variable detection (compiled once)
|
||||
_RE_CPP_SINGLE_LINE_COMMENT = re.compile(r"//.*?$", re.MULTILINE)
|
||||
_RE_CPP_MULTI_LINE_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
|
||||
_RE_STATIC_VARIABLE = re.compile(r"\bstatic\s+(?!cast|assert|pointer_cast)\w+\s+\w+")
|
||||
|
||||
|
||||
class RawExpression(Expression):
|
||||
__slots__ = ("text",)
|
||||
@@ -188,7 +198,7 @@ class LambdaExpression(Expression):
|
||||
|
||||
def __init__(
|
||||
self, parts, parameters, capture: str = "=", return_type=None, source=None
|
||||
):
|
||||
) -> None:
|
||||
self.parts = parts
|
||||
if not isinstance(parameters, ParameterListExpression):
|
||||
parameters = ParameterListExpression(*parameters)
|
||||
@@ -197,16 +207,21 @@ class LambdaExpression(Expression):
|
||||
self.capture = capture
|
||||
self.return_type = safe_exp(return_type) if return_type is not None else None
|
||||
|
||||
def __str__(self):
|
||||
def format_body(self) -> str:
|
||||
"""Format the lambda body with source directive and content."""
|
||||
body = ""
|
||||
if self.source is not None:
|
||||
body += f"{self.source.as_line_directive}\n"
|
||||
body += self.content
|
||||
return body
|
||||
|
||||
def __str__(self) -> str:
|
||||
# Stateless lambdas (empty capture) implicitly convert to function pointers
|
||||
# when assigned to function pointer types - no unary + needed
|
||||
cpp = f"[{self.capture}]({self.parameters})"
|
||||
if self.return_type is not None:
|
||||
cpp += f" -> {self.return_type}"
|
||||
cpp += " {\n"
|
||||
if self.source is not None:
|
||||
cpp += f"{self.source.as_line_directive}\n"
|
||||
cpp += f"{self.content}\n}}"
|
||||
cpp += f" {{\n{self.format_body()}\n}}"
|
||||
return indent_all_but_first_and_last(cpp)
|
||||
|
||||
@property
|
||||
@@ -214,6 +229,37 @@ class LambdaExpression(Expression):
|
||||
return "".join(str(part) for part in self.parts)
|
||||
|
||||
|
||||
class SharedFunctionLambdaExpression(LambdaExpression):
|
||||
"""A lambda expression that references a shared deduplicated function.
|
||||
|
||||
This class wraps a function pointer but maintains the LambdaExpression
|
||||
interface so calling code works unchanged.
|
||||
"""
|
||||
|
||||
__slots__ = ("_func_name",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func_name: str,
|
||||
parameters: TemplateArgsType,
|
||||
return_type: SafeExpType | None = None,
|
||||
) -> None:
|
||||
# Initialize parent with empty parts since we're just a function reference
|
||||
super().__init__(
|
||||
[], parameters, capture="", return_type=return_type, source=None
|
||||
)
|
||||
self._func_name = func_name
|
||||
|
||||
def __str__(self) -> str:
|
||||
# Just return the function name - it's already a function pointer
|
||||
return self._func_name
|
||||
|
||||
@property
|
||||
def content(self) -> str:
|
||||
# No content, just a function reference
|
||||
return ""
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class Literal(Expression, metaclass=abc.ABCMeta):
|
||||
__slots__ = ()
|
||||
@@ -583,6 +629,25 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False):
|
||||
CORE.add_global(expression, prepend)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def flush_lambda_dedup_declarations() -> None:
|
||||
"""Flush all deferred lambda deduplication declarations to global scope.
|
||||
|
||||
This is a coroutine that runs with FINAL priority (after all components)
|
||||
to ensure all referenced variables are declared before the shared
|
||||
lambda functions that use them.
|
||||
"""
|
||||
if _KEY_LAMBDA_DEDUP_DECLARATIONS not in CORE.data:
|
||||
return
|
||||
|
||||
declarations = CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS]
|
||||
for func_declaration in declarations:
|
||||
add_global(RawStatement(func_declaration))
|
||||
|
||||
# Clear the list so we don't add them again
|
||||
CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] = []
|
||||
|
||||
|
||||
def add_library(name: str, version: str | None, repository: str | None = None):
|
||||
"""Add a library to the codegen library storage.
|
||||
|
||||
@@ -656,6 +721,93 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
|
||||
return await CORE.get_variable_with_full_id(id_)
|
||||
|
||||
|
||||
def _has_static_variables(code: str) -> bool:
|
||||
"""Check if code contains static variable definitions.
|
||||
|
||||
Static variables in lambdas should not be deduplicated because each lambda
|
||||
instance should have its own static variable state.
|
||||
|
||||
Args:
|
||||
code: The lambda body code to check
|
||||
|
||||
Returns:
|
||||
True if code contains static variable definitions
|
||||
"""
|
||||
# Remove C++ comments to avoid false positives
|
||||
# Remove single-line comments (// ...)
|
||||
code_no_comments = _RE_CPP_SINGLE_LINE_COMMENT.sub("", code)
|
||||
# Remove multi-line comments (/* ... */)
|
||||
code_no_comments = _RE_CPP_MULTI_LINE_COMMENT.sub("", code_no_comments)
|
||||
|
||||
# Match: static <type> <identifier>
|
||||
# But not: static_cast, static_assert, static_pointer_cast
|
||||
return bool(_RE_STATIC_VARIABLE.search(code_no_comments))
|
||||
|
||||
|
||||
def _get_shared_lambda_name(lambda_expr: LambdaExpression) -> str | None:
|
||||
"""Get the shared function name for a lambda expression.
|
||||
|
||||
If an identical lambda was already generated, returns the existing shared
|
||||
function name. Otherwise, creates a new shared function and returns its name.
|
||||
|
||||
Lambdas with static variables are not deduplicated to preserve their
|
||||
independent state.
|
||||
|
||||
Args:
|
||||
lambda_expr: The lambda expression to deduplicate
|
||||
|
||||
Returns:
|
||||
The name of the shared function for this lambda (either existing or newly created),
|
||||
or None if the lambda should not be deduplicated (e.g., contains static variables)
|
||||
"""
|
||||
# Create a unique key from the lambda content, parameters, and return type
|
||||
content = lambda_expr.content
|
||||
|
||||
# Don't deduplicate lambdas with static variables - each instance needs its own state
|
||||
if _has_static_variables(content):
|
||||
return None
|
||||
param_str = str(lambda_expr.parameters)
|
||||
return_str = (
|
||||
str(lambda_expr.return_type) if lambda_expr.return_type is not None else "void"
|
||||
)
|
||||
|
||||
# Use tuple of (content, params, return_type) as key
|
||||
lambda_key = (content, param_str, return_str)
|
||||
|
||||
# Initialize deduplication storage in CORE.data if not exists
|
||||
if _KEY_LAMBDA_DEDUP not in CORE.data:
|
||||
CORE.data[_KEY_LAMBDA_DEDUP] = {}
|
||||
# Register the flush job to run after all components (FINAL priority)
|
||||
# This ensures all variables are declared before shared lambda functions
|
||||
CORE.add_job(flush_lambda_dedup_declarations)
|
||||
|
||||
lambda_cache = CORE.data[_KEY_LAMBDA_DEDUP]
|
||||
|
||||
# Check if we've seen this lambda before
|
||||
if lambda_key in lambda_cache:
|
||||
# Return name of existing shared function
|
||||
return lambda_cache[lambda_key]
|
||||
|
||||
# First occurrence - create a shared function
|
||||
# Use the cache size as the function number
|
||||
func_name = f"shared_lambda_{len(lambda_cache)}"
|
||||
|
||||
# Build the function declaration using lambda's body formatting
|
||||
func_declaration = (
|
||||
f"{return_str} {func_name}({param_str}) {{\n{lambda_expr.format_body()}\n}}"
|
||||
)
|
||||
|
||||
# Store the declaration to be added later (after all variable declarations)
|
||||
# We can't add it immediately because it might reference variables not yet declared
|
||||
CORE.data.setdefault(_KEY_LAMBDA_DEDUP_DECLARATIONS, []).append(func_declaration)
|
||||
|
||||
# Store in cache
|
||||
lambda_cache[lambda_key] = func_name
|
||||
|
||||
# Return the function name (this is the first occurrence, but we still generate shared function)
|
||||
return func_name
|
||||
|
||||
|
||||
async def process_lambda(
|
||||
value: Lambda,
|
||||
parameters: TemplateArgsType,
|
||||
@@ -713,6 +865,19 @@ async def process_lambda(
|
||||
location.line += value.content_offset
|
||||
else:
|
||||
location = None
|
||||
|
||||
# Lambda deduplication: Only deduplicate stateless lambdas (empty capture).
|
||||
# Stateful lambdas cannot be shared as they capture different contexts.
|
||||
# Lambdas with static variables are also not deduplicated to preserve independent state.
|
||||
if capture == "":
|
||||
lambda_expr = LambdaExpression(
|
||||
parts, parameters, capture, return_type, location
|
||||
)
|
||||
func_name = _get_shared_lambda_name(lambda_expr)
|
||||
if func_name is not None:
|
||||
# Return a shared function reference instead of inline lambda
|
||||
return SharedFunctionLambdaExpression(func_name, parameters, return_type)
|
||||
|
||||
return LambdaExpression(parts, parameters, capture, return_type, location)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Tests for the binary sensor component."""
|
||||
"""Tests for the text component."""
|
||||
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def test_text_is_setup(generate_main):
|
||||
@@ -56,15 +58,22 @@ def test_text_config_value_mode_set(generate_main):
|
||||
assert "it_3->traits.set_mode(text::TEXT_MODE_PASSWORD);" in main_cpp
|
||||
|
||||
|
||||
def test_text_config_lamda_is_set(generate_main):
|
||||
def test_text_config_lambda_is_set(generate_main) -> None:
|
||||
"""
|
||||
Test if lambda is set for lambda mode (optimized with stateless lambda)
|
||||
Test if lambda is set for lambda mode (optimized with stateless lambda and deduplication)
|
||||
"""
|
||||
# Given
|
||||
|
||||
# When
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Get both global and main sections to find the shared lambda definition
|
||||
full_cpp = CORE.cpp_global_section + main_cpp
|
||||
|
||||
# Then
|
||||
assert "it_4->set_template([]() -> esphome::optional<std::string> {" in main_cpp
|
||||
assert 'return std::string{"Hello"};' in main_cpp
|
||||
# Lambda is deduplicated into a shared function (reference in main section)
|
||||
assert "it_4->set_template(shared_lambda_" in main_cpp
|
||||
# Lambda body should be in the code somewhere
|
||||
assert 'return std::string{"Hello"};' in full_cpp
|
||||
# Verify the shared lambda function is defined (in global section)
|
||||
assert "esphome::optional<std::string> shared_lambda_" in full_cpp
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
esphome:
|
||||
name: test-timeout-filters
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
# Template sensors that we'll use to publish values
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "Source Timeout Last"
|
||||
id: source_timeout_last
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Timeout Reset"
|
||||
id: source_timeout_reset
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Timeout Static"
|
||||
id: source_timeout_static
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Timeout Lambda"
|
||||
id: source_timeout_lambda
|
||||
accuracy_decimals: 1
|
||||
|
||||
# Test 1: TimeoutFilter - "last" mode (outputs last received value)
|
||||
- platform: copy
|
||||
source_id: source_timeout_last
|
||||
name: "Timeout Last Sensor"
|
||||
id: timeout_last_sensor
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 100ms
|
||||
value: last # Explicitly specify "last" mode to use TimeoutFilter class
|
||||
|
||||
# Test 2: TimeoutFilter - reset behavior (same filter, different source)
|
||||
- platform: copy
|
||||
source_id: source_timeout_reset
|
||||
name: "Timeout Reset Sensor"
|
||||
id: timeout_reset_sensor
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 100ms
|
||||
value: last # Explicitly specify "last" mode
|
||||
|
||||
# Test 3: TimeoutFilterConfigured - static value mode
|
||||
- platform: copy
|
||||
source_id: source_timeout_static
|
||||
name: "Timeout Static Sensor"
|
||||
id: timeout_static_sensor
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 100ms
|
||||
value: 99.9
|
||||
|
||||
# Test 4: TimeoutFilterConfigured - lambda mode
|
||||
- platform: copy
|
||||
source_id: source_timeout_lambda
|
||||
name: "Timeout Lambda Sensor"
|
||||
id: timeout_lambda_sensor
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 100ms
|
||||
value: !lambda "return -1.0;"
|
||||
|
||||
# Scripts to publish values with controlled timing
|
||||
script:
|
||||
# Test 1: Single value followed by timeout
|
||||
- id: test_timeout_last_script
|
||||
then:
|
||||
# Publish initial value
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_last
|
||||
state: 42.0
|
||||
# Wait for timeout to fire (100ms + margin)
|
||||
- delay: 150ms
|
||||
|
||||
# Test 2: Multiple values before timeout (should reset timer)
|
||||
- id: test_timeout_reset_script
|
||||
then:
|
||||
# Publish first value
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_reset
|
||||
state: 10.0
|
||||
# Wait 50ms (halfway to timeout)
|
||||
- delay: 50ms
|
||||
# Publish second value (resets timeout)
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_reset
|
||||
state: 20.0
|
||||
# Wait 50ms (halfway to timeout again)
|
||||
- delay: 50ms
|
||||
# Publish third value (resets timeout)
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_reset
|
||||
state: 30.0
|
||||
# Wait for timeout to fire (100ms + margin)
|
||||
- delay: 150ms
|
||||
|
||||
# Test 3: Static value timeout
|
||||
- id: test_timeout_static_script
|
||||
then:
|
||||
# Publish initial value
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_static
|
||||
state: 55.5
|
||||
# Wait for timeout to fire
|
||||
- delay: 150ms
|
||||
|
||||
# Test 4: Lambda value timeout
|
||||
- id: test_timeout_lambda_script
|
||||
then:
|
||||
# Publish initial value
|
||||
- sensor.template.publish:
|
||||
id: source_timeout_lambda
|
||||
state: 77.7
|
||||
# Wait for timeout to fire
|
||||
- delay: 150ms
|
||||
|
||||
# Buttons to trigger each test scenario
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test Timeout Last Button"
|
||||
id: test_timeout_last_button
|
||||
on_press:
|
||||
- script.execute: test_timeout_last_script
|
||||
|
||||
- platform: template
|
||||
name: "Test Timeout Reset Button"
|
||||
id: test_timeout_reset_button
|
||||
on_press:
|
||||
- script.execute: test_timeout_reset_script
|
||||
|
||||
- platform: template
|
||||
name: "Test Timeout Static Button"
|
||||
id: test_timeout_static_button
|
||||
on_press:
|
||||
- script.execute: test_timeout_static_script
|
||||
|
||||
- platform: template
|
||||
name: "Test Timeout Lambda Button"
|
||||
id: test_timeout_lambda_button
|
||||
on_press:
|
||||
- script.execute: test_timeout_lambda_script
|
||||
@@ -1,185 +0,0 @@
|
||||
"""Test sensor timeout filter functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import EntityState, SensorState
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, build_key_to_entity_mapping
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sensor_timeout_filter(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test TimeoutFilter and TimeoutFilterConfigured with all modes."""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track state changes for all sensors
|
||||
timeout_last_states: list[float] = []
|
||||
timeout_reset_states: list[float] = []
|
||||
timeout_static_states: list[float] = []
|
||||
timeout_lambda_states: list[float] = []
|
||||
|
||||
# Futures for each test scenario
|
||||
test1_complete = loop.create_future() # TimeoutFilter - last mode
|
||||
test2_complete = loop.create_future() # TimeoutFilter - reset behavior
|
||||
test3_complete = loop.create_future() # TimeoutFilterConfigured - static value
|
||||
test4_complete = loop.create_future() # TimeoutFilterConfigured - lambda
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
"""Track sensor state updates."""
|
||||
if not isinstance(state, SensorState):
|
||||
return
|
||||
|
||||
if state.missing_state:
|
||||
return
|
||||
|
||||
sensor_name = key_to_sensor.get(state.key)
|
||||
|
||||
# Test 1: TimeoutFilter - last mode
|
||||
if sensor_name == "timeout_last_sensor":
|
||||
timeout_last_states.append(state.state)
|
||||
# Expect 2 values: initial 42.0 + timeout fires with 42.0
|
||||
if len(timeout_last_states) >= 2 and not test1_complete.done():
|
||||
test1_complete.set_result(True)
|
||||
|
||||
# Test 2: TimeoutFilter - reset behavior
|
||||
elif sensor_name == "timeout_reset_sensor":
|
||||
timeout_reset_states.append(state.state)
|
||||
# Expect 4 values: 10.0, 20.0, 30.0, then timeout fires with 30.0
|
||||
if len(timeout_reset_states) >= 4 and not test2_complete.done():
|
||||
test2_complete.set_result(True)
|
||||
|
||||
# Test 3: TimeoutFilterConfigured - static value
|
||||
elif sensor_name == "timeout_static_sensor":
|
||||
timeout_static_states.append(state.state)
|
||||
# Expect 2 values: initial 55.5 + timeout fires with 99.9
|
||||
if len(timeout_static_states) >= 2 and not test3_complete.done():
|
||||
test3_complete.set_result(True)
|
||||
|
||||
# Test 4: TimeoutFilterConfigured - lambda
|
||||
elif sensor_name == "timeout_lambda_sensor":
|
||||
timeout_lambda_states.append(state.state)
|
||||
# Expect 2 values: initial 77.7 + timeout fires with -1.0
|
||||
if len(timeout_lambda_states) >= 2 and not test4_complete.done():
|
||||
test4_complete.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, services = await client.list_entities_services()
|
||||
|
||||
key_to_sensor = build_key_to_entity_mapping(
|
||||
entities,
|
||||
[
|
||||
"timeout_last_sensor",
|
||||
"timeout_reset_sensor",
|
||||
"timeout_static_sensor",
|
||||
"timeout_lambda_sensor",
|
||||
],
|
||||
)
|
||||
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
# Helper to find buttons by object_id substring
|
||||
def find_button(object_id_substring: str) -> int:
|
||||
"""Find a button by object_id substring and return its key."""
|
||||
button = next(
|
||||
(e for e in entities if object_id_substring in e.object_id.lower()),
|
||||
None,
|
||||
)
|
||||
assert button is not None, f"Button '{object_id_substring}' not found"
|
||||
return button.key
|
||||
|
||||
# Find all test buttons
|
||||
test1_button_key = find_button("test_timeout_last_button")
|
||||
test2_button_key = find_button("test_timeout_reset_button")
|
||||
test3_button_key = find_button("test_timeout_static_button")
|
||||
test4_button_key = find_button("test_timeout_lambda_button")
|
||||
|
||||
# === Test 1: TimeoutFilter - last mode ===
|
||||
client.button_command(test1_button_key)
|
||||
try:
|
||||
await asyncio.wait_for(test1_complete, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 1 timeout. Received states: {timeout_last_states}")
|
||||
|
||||
assert len(timeout_last_states) == 2, (
|
||||
f"Test 1: Should have 2 states, got {len(timeout_last_states)}: {timeout_last_states}"
|
||||
)
|
||||
assert timeout_last_states[0] == pytest.approx(42.0), (
|
||||
f"Test 1: First state should be 42.0, got {timeout_last_states[0]}"
|
||||
)
|
||||
assert timeout_last_states[1] == pytest.approx(42.0), (
|
||||
f"Test 1: Timeout should output last value (42.0), got {timeout_last_states[1]}"
|
||||
)
|
||||
|
||||
# === Test 2: TimeoutFilter - reset behavior ===
|
||||
client.button_command(test2_button_key)
|
||||
try:
|
||||
await asyncio.wait_for(test2_complete, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 2 timeout. Received states: {timeout_reset_states}")
|
||||
|
||||
assert len(timeout_reset_states) == 4, (
|
||||
f"Test 2: Should have 4 states, got {len(timeout_reset_states)}: {timeout_reset_states}"
|
||||
)
|
||||
assert timeout_reset_states[0] == pytest.approx(10.0), (
|
||||
f"Test 2: First state should be 10.0, got {timeout_reset_states[0]}"
|
||||
)
|
||||
assert timeout_reset_states[1] == pytest.approx(20.0), (
|
||||
f"Test 2: Second state should be 20.0, got {timeout_reset_states[1]}"
|
||||
)
|
||||
assert timeout_reset_states[2] == pytest.approx(30.0), (
|
||||
f"Test 2: Third state should be 30.0, got {timeout_reset_states[2]}"
|
||||
)
|
||||
assert timeout_reset_states[3] == pytest.approx(30.0), (
|
||||
f"Test 2: Timeout should output last value (30.0), got {timeout_reset_states[3]}"
|
||||
)
|
||||
|
||||
# === Test 3: TimeoutFilterConfigured - static value ===
|
||||
client.button_command(test3_button_key)
|
||||
try:
|
||||
await asyncio.wait_for(test3_complete, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 3 timeout. Received states: {timeout_static_states}")
|
||||
|
||||
assert len(timeout_static_states) == 2, (
|
||||
f"Test 3: Should have 2 states, got {len(timeout_static_states)}: {timeout_static_states}"
|
||||
)
|
||||
assert timeout_static_states[0] == pytest.approx(55.5), (
|
||||
f"Test 3: First state should be 55.5, got {timeout_static_states[0]}"
|
||||
)
|
||||
assert timeout_static_states[1] == pytest.approx(99.9), (
|
||||
f"Test 3: Timeout should output configured value (99.9), got {timeout_static_states[1]}"
|
||||
)
|
||||
|
||||
# === Test 4: TimeoutFilterConfigured - lambda ===
|
||||
client.button_command(test4_button_key)
|
||||
try:
|
||||
await asyncio.wait_for(test4_complete, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 4 timeout. Received states: {timeout_lambda_states}")
|
||||
|
||||
assert len(timeout_lambda_states) == 2, (
|
||||
f"Test 4: Should have 2 states, got {len(timeout_lambda_states)}: {timeout_lambda_states}"
|
||||
)
|
||||
assert timeout_lambda_states[0] == pytest.approx(77.7), (
|
||||
f"Test 4: First state should be 77.7, got {timeout_lambda_states[0]}"
|
||||
)
|
||||
assert timeout_lambda_states[1] == pytest.approx(-1.0), (
|
||||
f"Test 4: Timeout should evaluate lambda (-1.0), got {timeout_lambda_states[1]}"
|
||||
)
|
||||
279
tests/unit_tests/test_lambda_dedup.py
Normal file
279
tests/unit_tests/test_lambda_dedup.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""Tests for lambda deduplication in cpp_generator."""
|
||||
|
||||
from esphome import cpp_generator as cg
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def test_deduplicate_identical_lambdas() -> None:
|
||||
"""Test that identical stateless lambdas are deduplicated."""
|
||||
# Create two identical lambda expressions
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["return 42;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
lambda2 = cg.LambdaExpression(
|
||||
parts=["return 42;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
# Try to deduplicate them
|
||||
func_name1 = cg._get_shared_lambda_name(lambda1)
|
||||
func_name2 = cg._get_shared_lambda_name(lambda2)
|
||||
|
||||
# Both should get the same function name (deduplication happened)
|
||||
assert func_name1 == func_name2
|
||||
assert func_name1 == "shared_lambda_0"
|
||||
|
||||
|
||||
def test_different_lambdas_not_deduplicated() -> None:
|
||||
"""Test that different lambdas get different function names."""
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["return 42;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
lambda2 = cg.LambdaExpression(
|
||||
parts=["return 24;"], # Different content
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
func_name1 = cg._get_shared_lambda_name(lambda1)
|
||||
func_name2 = cg._get_shared_lambda_name(lambda2)
|
||||
|
||||
# Different lambdas should get different function names
|
||||
assert func_name1 != func_name2
|
||||
assert func_name1 == "shared_lambda_0"
|
||||
assert func_name2 == "shared_lambda_1"
|
||||
|
||||
|
||||
def test_different_return_types_not_deduplicated() -> None:
|
||||
"""Test that lambdas with different return types are not deduplicated."""
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["return 42;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
lambda2 = cg.LambdaExpression(
|
||||
parts=["return 42;"], # Same content
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("float"), # Different return type
|
||||
)
|
||||
|
||||
func_name1 = cg._get_shared_lambda_name(lambda1)
|
||||
func_name2 = cg._get_shared_lambda_name(lambda2)
|
||||
|
||||
# Different return types = different functions
|
||||
assert func_name1 != func_name2
|
||||
|
||||
|
||||
def test_different_parameters_not_deduplicated() -> None:
|
||||
"""Test that lambdas with different parameters are not deduplicated."""
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["return x;"],
|
||||
parameters=[("int", "x")],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
lambda2 = cg.LambdaExpression(
|
||||
parts=["return x;"], # Same content
|
||||
parameters=[("float", "x")], # Different parameter type
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
func_name1 = cg._get_shared_lambda_name(lambda1)
|
||||
func_name2 = cg._get_shared_lambda_name(lambda2)
|
||||
|
||||
# Different parameters = different functions
|
||||
assert func_name1 != func_name2
|
||||
|
||||
|
||||
def test_flush_lambda_dedup_declarations() -> None:
|
||||
"""Test that deferred declarations are properly stored for later flushing."""
|
||||
# Create a lambda which will create a deferred declaration
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["return 42;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
cg._get_shared_lambda_name(lambda1)
|
||||
|
||||
# Check that declaration was stored
|
||||
assert cg._KEY_LAMBDA_DEDUP_DECLARATIONS in CORE.data
|
||||
assert len(CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS]) == 1
|
||||
|
||||
# Verify the declaration content is correct
|
||||
declaration = CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS][0]
|
||||
assert "shared_lambda_0" in declaration
|
||||
assert "return 42;" in declaration
|
||||
|
||||
# Note: The actual flushing happens via CORE.add_job with FINAL priority
|
||||
# during real code generation, so we don't test that here
|
||||
|
||||
|
||||
def test_shared_function_lambda_expression() -> None:
|
||||
"""Test SharedFunctionLambdaExpression behaves correctly."""
|
||||
shared_lambda = cg.SharedFunctionLambdaExpression(
|
||||
func_name="shared_lambda_0",
|
||||
parameters=[],
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
# Should output just the function name
|
||||
assert str(shared_lambda) == "shared_lambda_0"
|
||||
|
||||
# Should have empty capture (stateless)
|
||||
assert shared_lambda.capture == ""
|
||||
|
||||
# Should have empty content (just a reference)
|
||||
assert shared_lambda.content == ""
|
||||
|
||||
|
||||
def test_lambda_deduplication_counter() -> None:
|
||||
"""Test that lambda counter increments correctly."""
|
||||
# Create 3 different lambdas
|
||||
for i in range(3):
|
||||
lambda_expr = cg.LambdaExpression(
|
||||
parts=[f"return {i};"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
func_name = cg._get_shared_lambda_name(lambda_expr)
|
||||
assert func_name == f"shared_lambda_{i}"
|
||||
|
||||
|
||||
def test_lambda_format_body() -> None:
|
||||
"""Test that format_body correctly formats lambda body with source."""
|
||||
# Without source
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["return 42;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=None,
|
||||
source=None,
|
||||
)
|
||||
assert lambda1.format_body() == "return 42;"
|
||||
|
||||
# With source would need a proper source object, skip for now
|
||||
|
||||
|
||||
def test_stateful_lambdas_not_deduplicated() -> None:
|
||||
"""Test that stateful lambdas (non-empty capture) are not deduplicated."""
|
||||
# _get_shared_lambda_name is only called for stateless lambdas (capture == "")
|
||||
# Stateful lambdas bypass deduplication entirely in process_lambda
|
||||
|
||||
# Verify that a stateful lambda would NOT get deduplicated
|
||||
# by checking it's not in the stateless dedup cache
|
||||
stateful_lambda = cg.LambdaExpression(
|
||||
parts=["return x + y;"],
|
||||
parameters=[],
|
||||
capture="=", # Non-empty capture means stateful
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
# Stateful lambdas should NOT be passed to _get_shared_lambda_name
|
||||
# This is enforced by the `if capture == ""` check in process_lambda
|
||||
# We verify the lambda has a non-empty capture
|
||||
assert stateful_lambda.capture != ""
|
||||
assert stateful_lambda.capture == "="
|
||||
|
||||
|
||||
def test_static_variable_detection() -> None:
|
||||
"""Test detection of static variables in lambda code."""
|
||||
# Should detect static variables
|
||||
assert cg._has_static_variables("static int counter = 0;")
|
||||
assert cg._has_static_variables("static bool flag = false; return flag;")
|
||||
assert cg._has_static_variables(" static float value = 1.0; ")
|
||||
|
||||
# Should NOT detect static_cast, static_assert, etc. (with underscores)
|
||||
assert not cg._has_static_variables("return static_cast<int>(value);")
|
||||
assert not cg._has_static_variables("static_assert(sizeof(int) == 4);")
|
||||
assert not cg._has_static_variables("auto ptr = static_pointer_cast<Foo>(bar);")
|
||||
|
||||
# Edge case: 'cast', 'assert', 'pointer_cast' are NOT C++ keywords
|
||||
# Someone could use them as type names, but we should NOT flag them
|
||||
# because they're not actually static variables with state
|
||||
# NOTE: These are valid C++ but extremely unlikely in ESPHome lambdas
|
||||
assert not cg._has_static_variables("static cast obj;") # 'cast' as type name
|
||||
assert not cg._has_static_variables("static assert value;") # 'assert' as type name
|
||||
assert not cg._has_static_variables(
|
||||
"static pointer_cast ptr;"
|
||||
) # 'pointer_cast' as type
|
||||
|
||||
# Should NOT detect in comments
|
||||
assert not cg._has_static_variables("// static int x = 0;\nreturn 42;")
|
||||
assert not cg._has_static_variables("/* static int y = 0; */ return 42;")
|
||||
|
||||
# Should detect even with comments elsewhere
|
||||
assert cg._has_static_variables("// comment\nstatic int x = 0;\nreturn x;")
|
||||
|
||||
# Should NOT detect non-static code
|
||||
assert not cg._has_static_variables("int counter = 0; return counter++;")
|
||||
assert not cg._has_static_variables("return 42;")
|
||||
|
||||
|
||||
def test_lambdas_with_static_not_deduplicated() -> None:
|
||||
"""Test that lambdas with static variables are not deduplicated."""
|
||||
# Two identical lambdas with static variables
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["static int counter = 0; return counter++;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
lambda2 = cg.LambdaExpression(
|
||||
parts=["static int counter = 0; return counter++;"],
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
# Should return None (not deduplicated)
|
||||
func_name1 = cg._get_shared_lambda_name(lambda1)
|
||||
func_name2 = cg._get_shared_lambda_name(lambda2)
|
||||
|
||||
assert func_name1 is None
|
||||
assert func_name2 is None
|
||||
|
||||
|
||||
def test_lambdas_without_static_still_deduplicated() -> None:
|
||||
"""Test that lambdas without static variables are still deduplicated."""
|
||||
# Two identical lambdas WITHOUT static variables
|
||||
lambda1 = cg.LambdaExpression(
|
||||
parts=["int counter = 0; return counter++;"], # No static
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
lambda2 = cg.LambdaExpression(
|
||||
parts=["int counter = 0; return counter++;"], # No static
|
||||
parameters=[],
|
||||
capture="",
|
||||
return_type=cg.RawExpression("int"),
|
||||
)
|
||||
|
||||
# Should be deduplicated (same function name)
|
||||
func_name1 = cg._get_shared_lambda_name(lambda1)
|
||||
func_name2 = cg._get_shared_lambda_name(lambda2)
|
||||
|
||||
assert func_name1 is not None
|
||||
assert func_name2 is not None
|
||||
assert func_name1 == func_name2
|
||||
Reference in New Issue
Block a user