From 344992c670e329f6033b1f8075d7b88540d8174b Mon Sep 17 00:00:00 2001 From: tetele Date: Sat, 23 Nov 2024 01:13:25 +0000 Subject: [PATCH] Self calibration for ESP32 touch controls --- .../components/esp32_touch/binary_sensor.py | 75 +++++++++++++++++-- .../components/esp32_touch/esp32_touch.cpp | 55 ++++++++++++++ esphome/components/esp32_touch/esp32_touch.h | 17 +++++ 3 files changed, 142 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32_touch/binary_sensor.py b/esphome/components/esp32_touch/binary_sensor.py index e9322b3080..e1bea84491 100644 --- a/esphome/components/esp32_touch/binary_sensor.py +++ b/esphome/components/esp32_touch/binary_sensor.py @@ -1,27 +1,74 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import binary_sensor +import esphome.config_validation as cv from esphome.const import ( + CONF_ID, + CONF_INITIAL_VALUE, + CONF_MODE, CONF_PIN, CONF_THRESHOLD, - CONF_ID, + CONF_VALUE, ) -from . import esp32_touch_ns, ESP32TouchComponent, validate_touch_pad + +from . import ESP32TouchComponent, esp32_touch_ns, validate_touch_pad DEPENDENCIES = ["esp32_touch", "esp32"] CONF_ESP32_TOUCH_ID = "esp32_touch_id" CONF_WAKEUP_THRESHOLD = "wakeup_threshold" +CONF_LOOKBACK_NUM_VALUES = "lookback_num_values" +CONF_SCAN_INTERVAL = "scan_interval" +CONF_MAX_DEVIATION = "max_deviation" +CONF_MAX_CONSECUTIVE_ANOMALIES = "max_consecutive_anomalies" + +THRESHOLD_MODE_STATIC = "static" +THRESHOLD_MODE_DYNAMIC = "dynamic" ESP32TouchBinarySensor = esp32_touch_ns.class_( "ESP32TouchBinarySensor", binary_sensor.BinarySensor ) +THRESHOLD_SCHEMA_STATIC = cv.Schema( + { + cv.Required(CONF_MODE): THRESHOLD_MODE_STATIC, + cv.Required(CONF_VALUE): cv.uint32_t, + } +) + +THRESHOLD_SCHEMA_DYNAMIC = cv.Schema( + { + cv.Required(CONF_MODE): THRESHOLD_MODE_DYNAMIC, + cv.Optional(CONF_INITIAL_VALUE, default=0): cv.uint32_t, + cv.Optional(CONF_LOOKBACK_NUM_VALUES, default=5): cv.int_range(min=1), + cv.Optional( + CONF_SCAN_INTERVAL, default="1s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_MAX_DEVIATION, default="0.5%"): cv.percentage, + cv.Optional(CONF_MAX_CONSECUTIVE_ANOMALIES, default=10): cv.positive_int, + } +) + + +def _convert_int_to_threshold(config): + """Convert legacy threshold value to new format.""" + try: + threshold_value = cv.uint32_t(config) + return {CONF_MODE: THRESHOLD_MODE_STATIC, CONF_VALUE: threshold_value} + except cv.Invalid: + return config + + CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(ESP32TouchBinarySensor).extend( { cv.GenerateID(CONF_ESP32_TOUCH_ID): cv.use_id(ESP32TouchComponent), cv.Required(CONF_PIN): validate_touch_pad, - cv.Required(CONF_THRESHOLD): cv.uint32_t, + cv.Required(CONF_THRESHOLD): cv.All( + _convert_int_to_threshold, + cv.Any( + THRESHOLD_SCHEMA_STATIC.schema, + THRESHOLD_SCHEMA_DYNAMIC.schema, + ), + ), cv.Optional(CONF_WAKEUP_THRESHOLD, default=0): cv.uint32_t, } ) @@ -32,8 +79,26 @@ async def to_code(config): var = cg.new_Pvariable( config[CONF_ID], config[CONF_PIN], - config[CONF_THRESHOLD], + ( + config[CONF_THRESHOLD][CONF_VALUE] + if config[CONF_THRESHOLD][CONF_MODE] == THRESHOLD_MODE_STATIC + else config[CONF_THRESHOLD][CONF_INITIAL_VALUE] + ), config[CONF_WAKEUP_THRESHOLD], ) await binary_sensor.register_binary_sensor(var, config) cg.add(hub.register_touch_pad(var)) + + if config[CONF_THRESHOLD][CONF_MODE] == THRESHOLD_MODE_DYNAMIC: + cg.add(var.set_max_deviation(config[CONF_THRESHOLD][CONF_MAX_DEVIATION])) + cg.add( + var.set_max_consecutive_anomalies( + config[CONF_THRESHOLD][CONF_MAX_CONSECUTIVE_ANOMALIES] + ) + ) + cg.add( + var.start_calibration( + config[CONF_THRESHOLD][CONF_SCAN_INTERVAL], + config[CONF_THRESHOLD][CONF_LOOKBACK_NUM_VALUES], + ) + ) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index e43c3b844c..52ad1e2639 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -287,6 +287,10 @@ void ESP32TouchComponent::loop() { bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250; for (auto *child : this->children_) { child->value_ = this->component_touch_pad_read(child->get_touch_pad()); + if (child->dynamic_calibration_ && (now - child->last_calibration_timestamp_ > child->calibration_interval_)) { + child->insert_value_(); + child->last_calibration_timestamp_ += child->last_calibration_timestamp_ ? child->calibration_interval_ : now; + } #if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) child->publish_state(child->value_ < child->get_threshold()); #else @@ -340,6 +344,57 @@ void ESP32TouchComponent::on_shutdown() { ESP32TouchBinarySensor::ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold) : touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} +void ESP32TouchBinarySensor::set_max_deviation(float max_deviation) { this->max_deviation_ = max_deviation; } + +void ESP32TouchBinarySensor::set_max_consecutive_anomalies(float max_consecutive_anomalies) { + this->max_consecutive_anomalies_ = max_consecutive_anomalies; +} + +void ESP32TouchBinarySensor::start_calibration(uint32_t interval, uint16_t num_values) { + this->dynamic_calibration_ = true; + this->max_prev_values_ = num_values; + this->prev_values_.resize(0); + this->prev_values_.push_front(this->threshold_); + this->sum_values_ = this->threshold_; + this->calibration_interval_ = interval; + this->last_calibration_timestamp_ = 0; + this->consecutive_anomalies_ = 0; +} + +float ESP32TouchBinarySensor::get_average_value_() { + if (this->prev_values_.size() == 0) + return 0; + + return uint32_t(this->sum_values_ / float(this->prev_values_.size())); +} + +void ESP32TouchBinarySensor::insert_value_() { + float avg = this->get_average_value_(); + if (fabs(float(this->value_) - avg) / avg > this->max_deviation_) { + this->consecutive_anomalies_++; + if (this->consecutive_anomalies_ < this->max_consecutive_anomalies_) + return; + } + + this->consecutive_anomalies_ = 0; + + this->prev_values_.push_front(this->value_); + this->sum_values_ += this->value_; + + while (this->prev_values_.size() > this->max_prev_values_) { + this->sum_values_ -= this->prev_values_.back(); + this->prev_values_.pop_back(); + } + +#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) + this->threshold_ = + uint32_t(float(this->sum_values_) / float(this->prev_values_.size()) * (1.0 - this->max_deviation_)); +#else + this->threshold_ = + uint32_t(float(this->sum_values_) / float(this->prev_values_.size()) * (1.0 + this->max_deviation_)); +#endif +} + } // namespace esp32_touch } // namespace esphome diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 0eac590ce7..8d13d32a17 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -7,6 +7,7 @@ #include #include +#include #include @@ -106,6 +107,9 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { void set_threshold(uint32_t threshold) { this->threshold_ = threshold; } uint32_t get_value() const { return this->value_; } uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; } + void set_max_deviation(float max_deviation); + void set_max_consecutive_anomalies(float max_consecutive_anomalies); + void start_calibration(uint32_t interval, uint16_t num_values); protected: friend ESP32TouchComponent; @@ -114,6 +118,19 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { uint32_t threshold_{0}; uint32_t value_{0}; const uint32_t wakeup_threshold_{0}; + + bool dynamic_calibration_{false}; + float max_deviation_{0}; + std::deque prev_values_; + uint32_t sum_values_{0}; + uint16_t max_prev_values_{0}; + uint32_t last_calibration_timestamp_{0}; + uint32_t calibration_interval_{0}; + uint16_t consecutive_anomalies_{0}; + uint16_t max_consecutive_anomalies_{0}; + + float get_average_value_(); + void insert_value_(); }; } // namespace esp32_touch