diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 56e6b7dc97..5d4b227d70 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -4,6 +4,7 @@ from esphome import automation, core from esphome.automation import Condition, maybe_simple_id from esphome.components import mqtt from esphome.const import ( + CONF_DELAY, CONF_DEVICE_CLASS, CONF_FILTERS, CONF_ID, @@ -120,6 +121,7 @@ DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Co DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component) DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Component) InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) +AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) FILTER_REGISTRY = Registry() @@ -158,6 +160,51 @@ def delayed_off_filter_to_code(config, filter_id): yield var +CONF_TIME_OFF = "time_off" +CONF_TIME_ON = "time_on" + +DEFAULT_DELAY = "1s" +DEFAULT_TIME_OFF = "100ms" +DEFAULT_TIME_ON = "900ms" + + +@FILTER_REGISTRY.register( + "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, + } + ), + ), +) +def autorepeat_filter_to_code(config, filter_id): + timings = [] + if len(config) > 0: + for conf in config: + timings.append((conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])) + else: + timings.append( + ( + cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds, + cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds, + cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds, + ) + ) + var = cg.new_Pvariable(filter_id, timings) + yield cg.register_component(var, {}) + yield var + + @FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda) def lambda_filter_to_code(config, filter_id): lambda_ = yield cg.process_lambda( diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index f4612d62e9..c6ca3e2f79 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -64,6 +64,50 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD optional InvertFilter::new_value(bool value, bool is_initial) { return !value; } +AutorepeatFilter::AutorepeatFilter(const std::vector &timings) : timings_(timings) {} + +optional AutorepeatFilter::new_value(bool value, bool is_initial) { + if (value) { + // Ignore if already running + if (this->active_timing_ != 0) + return {}; + + this->next_timing_(); + return true; + } else { + this->cancel_timeout("TIMING"); + this->cancel_timeout("ON_OFF"); + this->active_timing_ = 0; + return false; + } +} + +void AutorepeatFilter::next_timing_() { + // Entering this method + // 1st time: starts waiting the first delay + // 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on + // last time: no delay to start but have to bump the index to reflect the last + if (this->active_timing_ < this->timings_.size()) + this->set_timeout("TIMING", this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); }); + + if (this->active_timing_ <= this->timings_.size()) { + this->active_timing_++; + } + + if (this->active_timing_ == 2) + this->next_value_(false); + + // Leaving this method: if the toggling is started, it has to use [active_timing_ - 2] for the intervals +} + +void AutorepeatFilter::next_value_(bool val) { + const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; + this->output(val, false); // This is at least the second one so not initial + this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); +} + +float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } + LambdaFilter::LambdaFilter(const std::function(bool)> &f) : f_(f) {} optional LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); } diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 0b54251cda..8528e74a9f 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -66,6 +66,33 @@ class InvertFilter : public Filter { optional new_value(bool value, bool is_initial) override; }; +struct AutorepeatFilterTiming { + AutorepeatFilterTiming(uint32_t delay, uint32_t off, uint32_t on) { + this->delay = delay; + this->time_off = off; + this->time_on = on; + } + uint32_t delay; + uint32_t time_off; + uint32_t time_on; +}; + +class AutorepeatFilter : public Filter, public Component { + public: + explicit AutorepeatFilter(const std::vector &timings); + + optional new_value(bool value, bool is_initial) override; + + float get_setup_priority() const override; + + protected: + void next_timing_(); + void next_value_(bool val); + + std::vector timings_; + uint8_t active_timing_{0}; +}; + class LambdaFilter : public Filter { public: explicit LambdaFilter(const std::function(bool)> &f); diff --git a/tests/test4.yaml b/tests/test4.yaml index e85cdfbc19..d2dcb8b682 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -86,6 +86,20 @@ binary_sensor: - platform: tuya id: tuya_binary_sensor sensor_datapoint: 1 + - platform: template + id: ar1 + lambda: 'return {};' + filters: + - autorepeat: + - delay: 2s + time_off: 100ms + time_on: 900ms + - delay: 4s + time_off: 100ms + time_on: 400ms + on_state: + then: + - lambda: 'ESP_LOGI("ar1:", "%d", x);' climate: - platform: tuya