mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 18:22:48 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0b0cd6e13 | ||
|
|
18e93a96fc | ||
|
|
01f1139476 | ||
|
|
053cb337aa | ||
|
|
3ac5918ec8 | ||
|
|
bf5d095cf6 | ||
|
|
eae1b99232 | ||
|
|
15936f9a55 | ||
|
|
aac085c5c7 | ||
|
|
fe66ac546a | ||
|
|
a8e5fd0d2f | ||
|
|
93afb0d092 |
@@ -146,6 +146,7 @@ DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter)
|
||||
DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter)
|
||||
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
|
||||
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter)
|
||||
AutorepeatFilterTiming = binary_sensor_ns.struct("AutorepeatFilterTiming")
|
||||
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
|
||||
StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter)
|
||||
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter)
|
||||
@@ -233,53 +234,49 @@ async def delayed_off_filter_to_code(config, filter_id):
|
||||
return var
|
||||
|
||||
|
||||
AUTOREPEAT_TIMING_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_DELAY, default=DEFAULT_DELAY
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TIME_OFF, default=DEFAULT_TIME_OFF
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TIME_ON, default=DEFAULT_TIME_ON
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@register_filter(
|
||||
"autorepeat",
|
||||
AutorepeatFilter,
|
||||
cv.All(
|
||||
cv.ensure_list(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_DELAY, default=DEFAULT_DELAY
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TIME_OFF, default=DEFAULT_TIME_OFF
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TIME_ON, default=DEFAULT_TIME_ON
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
cv.Length(max=254),
|
||||
),
|
||||
cv.All(cv.ensure_list(AUTOREPEAT_TIMING_SCHEMA), cv.Length(max=254)),
|
||||
)
|
||||
async def autorepeat_filter_to_code(config, filter_id):
|
||||
if len(config) > 0:
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
||||
("delay", conf[CONF_DELAY]),
|
||||
("time_off", conf[CONF_TIME_OFF]),
|
||||
("time_on", conf[CONF_TIME_ON]),
|
||||
)
|
||||
for conf in config
|
||||
]
|
||||
else:
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
||||
("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds),
|
||||
(
|
||||
"time_off",
|
||||
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
|
||||
),
|
||||
(
|
||||
"time_on",
|
||||
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
|
||||
),
|
||||
)
|
||||
]
|
||||
return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(timings)), timings)
|
||||
async def autorepeat_filter_to_code(config: list[ConfigType], filter_id: ID) -> MockObj:
|
||||
config = config or [AUTOREPEAT_TIMING_SCHEMA({})]
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
AutorepeatFilterTiming,
|
||||
("delay", conf[CONF_DELAY].total_milliseconds),
|
||||
("time_off", conf[CONF_TIME_OFF].total_milliseconds),
|
||||
("time_on", conf[CONF_TIME_ON].total_milliseconds),
|
||||
)
|
||||
for conf in config
|
||||
]
|
||||
# A never-run delay ends the table, so the filter stores no count.
|
||||
timings.append(
|
||||
cg.StructInitializer(
|
||||
AutorepeatFilterTiming, ("delay", cg.RawExpression("SCHEDULER_DONT_RUN"))
|
||||
)
|
||||
)
|
||||
table = cg.shared_progmem_array(
|
||||
"binary_sensor_autorepeat_timings",
|
||||
AutorepeatFilterTiming,
|
||||
cg.ArrayInitializer(*timings),
|
||||
)
|
||||
return cg.new_Pvariable(filter_id, table)
|
||||
|
||||
|
||||
@register_filter("lambda", LambdaFilter, cv.returning_lambda)
|
||||
|
||||
@@ -63,12 +63,12 @@ optional<bool> DelayedOffFilter::new_value(bool value) {
|
||||
|
||||
optional<bool> InvertFilter::new_value(bool value) { return !value; }
|
||||
|
||||
// AutorepeatFilterBase
|
||||
// AutorepeatFilter
|
||||
// Two independent timers per instance, keyed off two stable addresses inside
|
||||
// the filter: `this` for the timing-step timer, `&active_timing_` for the
|
||||
// on/off timer. Both are unique per instance and don't collide with anything
|
||||
// else, so the self-keyed scheduler API is sufficient.
|
||||
optional<bool> AutorepeatFilterBase::new_value(bool value) {
|
||||
optional<bool> AutorepeatFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
if (this->active_timing_ != 0)
|
||||
return {};
|
||||
@@ -82,18 +82,18 @@ optional<bool> AutorepeatFilterBase::new_value(bool value) {
|
||||
}
|
||||
}
|
||||
|
||||
void AutorepeatFilterBase::next_timing_() {
|
||||
if (this->active_timing_ < this->timings_count_) {
|
||||
App.scheduler.set_timeout(this, this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); });
|
||||
}
|
||||
if (this->active_timing_ <= this->timings_count_) {
|
||||
this->active_timing_++;
|
||||
void AutorepeatFilter::next_timing_() {
|
||||
const uint32_t delay = this->timings_[this->active_timing_].delay;
|
||||
if (delay != SCHEDULER_DONT_RUN) {
|
||||
App.scheduler.set_timeout(this, delay, [this]() { this->next_timing_(); });
|
||||
}
|
||||
// Only reached with active_timing_ at most the end-marker index, so this never passes it.
|
||||
this->active_timing_++;
|
||||
if (this->active_timing_ == 2)
|
||||
this->next_value_(false);
|
||||
}
|
||||
|
||||
void AutorepeatFilterBase::next_value_(bool val) {
|
||||
void AutorepeatFilter::next_value_(bool val) {
|
||||
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
|
||||
this->output(val);
|
||||
App.scheduler.set_timeout(&this->active_timing_, val ? timing.time_on : timing.time_off,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BINARY_SENSOR_FILTER
|
||||
|
||||
#include <array>
|
||||
#include <type_traits>
|
||||
#ifdef USE_BINARY_SENSOR_FILTER
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
@@ -87,39 +87,34 @@ struct AutorepeatFilterTiming {
|
||||
uint32_t time_off;
|
||||
uint32_t time_on;
|
||||
};
|
||||
// Read straight from flash on ESP8266, so every field must stay a word.
|
||||
static_assert(std::is_same_v<decltype(AutorepeatFilterTiming::delay), uint32_t>,
|
||||
"AutorepeatFilterTiming fields must stay uint32_t");
|
||||
static_assert(std::is_same_v<decltype(AutorepeatFilterTiming::time_off), uint32_t>,
|
||||
"AutorepeatFilterTiming fields must stay uint32_t");
|
||||
static_assert(std::is_same_v<decltype(AutorepeatFilterTiming::time_on), uint32_t>,
|
||||
"AutorepeatFilterTiming fields must stay uint32_t");
|
||||
static_assert(sizeof(AutorepeatFilterTiming) == 3 * sizeof(uint32_t),
|
||||
"AutorepeatFilterTiming is read from flash with word loads");
|
||||
|
||||
/// Non-template base for AutorepeatFilter — all methods in filter.cpp.
|
||||
/// Lambdas capture this base pointer, so set_timeout/cancel_timeout are instantiated once.
|
||||
/// Timings live in a PROGMEM table emitted by codegen, ended by an entry whose delay is
|
||||
/// SCHEDULER_DONT_RUN (a step that could never advance anyway). Aligned loads are ESP8266 safe.
|
||||
/// The two scheduled timers are keyed off `this` and `&active_timing_`; since the address
|
||||
/// of `active_timing_` is taken as a scheduler key, the class must not be copied or moved.
|
||||
class AutorepeatFilterBase : public Filter {
|
||||
class AutorepeatFilter : public Filter {
|
||||
public:
|
||||
explicit AutorepeatFilter(const AutorepeatFilterTiming *timings) : timings_(timings) {}
|
||||
AutorepeatFilter(const AutorepeatFilter &) = delete;
|
||||
AutorepeatFilter &operator=(const AutorepeatFilter &) = delete;
|
||||
|
||||
optional<bool> new_value(bool value) override;
|
||||
AutorepeatFilterBase(const AutorepeatFilterBase &) = delete;
|
||||
AutorepeatFilterBase &operator=(const AutorepeatFilterBase &) = delete;
|
||||
|
||||
protected:
|
||||
AutorepeatFilterBase() = default;
|
||||
void next_timing_();
|
||||
void next_value_(bool val);
|
||||
|
||||
const AutorepeatFilterTiming *timings_{nullptr};
|
||||
uint8_t timings_count_{0};
|
||||
uint8_t active_timing_{0};
|
||||
};
|
||||
|
||||
/// Template wrapper that provides inline std::array storage for timings.
|
||||
/// N is set by code generation to match the exact number of timings configured in YAML.
|
||||
template<size_t N> class AutorepeatFilter : public AutorepeatFilterBase {
|
||||
public:
|
||||
explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) {
|
||||
init_array_from(this->timings_storage_, timings);
|
||||
this->timings_ = this->timings_storage_.data();
|
||||
this->timings_count_ = N;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::array<AutorepeatFilterTiming, N> timings_storage_{};
|
||||
uint8_t active_timing_{0}; // before timings_ so it fills the padding after the Filter base
|
||||
const AutorepeatFilterTiming *timings_;
|
||||
};
|
||||
|
||||
class LambdaFilter : public Filter {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Tests for the binary sensor autorepeat filter codegen."""
|
||||
|
||||
from collections.abc import Callable
|
||||
import re
|
||||
|
||||
|
||||
def test_autorepeat_timings_shared_progmem_table(
|
||||
generate_main: Callable[[str], str],
|
||||
) -> None:
|
||||
"""Identical timings share one PROGMEM table; an empty autorepeat gets the defaults."""
|
||||
main_cpp = generate_main("tests/component_tests/binary_sensor/test_autorepeat.yaml")
|
||||
|
||||
tables = re.findall(
|
||||
r"static constexpr binary_sensor::AutorepeatFilterTiming (\w+)\[\] PROGMEM",
|
||||
main_cpp,
|
||||
)
|
||||
assert len(tables) == 2
|
||||
shared, default = tables
|
||||
assert main_cpp.count(f"AutorepeatFilter({shared});") == 2
|
||||
assert f"AutorepeatFilter({default});" in main_cpp
|
||||
assert "binary_sensor_autorepeat_timings" not in tables
|
||||
default_table = main_cpp.split(f"{default}[] PROGMEM = ", 1)[1].split(";", 1)[0]
|
||||
for field in (".delay = 1000", ".time_off = 100", ".time_on = 900"):
|
||||
assert field in default_table
|
||||
# Each table ends with a never-run delay instead of the filter storing a count.
|
||||
assert main_cpp.count(".delay = SCHEDULER_DONT_RUN") == len(tables)
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: repeat_a
|
||||
filters:
|
||||
- autorepeat:
|
||||
- delay: 2s
|
||||
time_off: 200ms
|
||||
time_on: 800ms
|
||||
- delay: 5s
|
||||
time_off: 100ms
|
||||
time_on: 400ms
|
||||
- platform: template
|
||||
id: repeat_b
|
||||
filters:
|
||||
- autorepeat:
|
||||
- delay: 2s
|
||||
time_off: 200ms
|
||||
time_on: 800ms
|
||||
- delay: 5s
|
||||
time_off: 100ms
|
||||
time_on: 400ms
|
||||
- platform: template
|
||||
id: repeat_default
|
||||
filters:
|
||||
- autorepeat:
|
||||
# A user id equal to the table's preferred name must not break codegen.
|
||||
- platform: template
|
||||
id: binary_sensor_autorepeat_timings
|
||||
Reference in New Issue
Block a user