diff --git a/CODEOWNERS b/CODEOWNERS index a5a4fc2552..026a57a423 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -113,6 +113,7 @@ esphome/components/integration/* @OttoWinter esphome/components/interval/* @esphome/core esphome/components/json/* @OttoWinter esphome/components/kalman_combinator/* @Cat-Ion +esphome/components/key_collector/* @ssieb esphome/components/key_provider/* @ssieb esphome/components/lcd_menu/* @numo68 esphome/components/ledc/* @OttoWinter diff --git a/esphome/components/key_collector/__init__.py b/esphome/components/key_collector/__init__.py new file mode 100644 index 0000000000..2099e28109 --- /dev/null +++ b/esphome/components/key_collector/__init__.py @@ -0,0 +1,95 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import key_provider +from esphome.const import ( + CONF_ID, + CONF_MAX_LENGTH, + CONF_MIN_LENGTH, + CONF_ON_TIMEOUT, + CONF_SOURCE_ID, + CONF_TIMEOUT, +) + +CODEOWNERS = ["@ssieb"] + +MULTI_CONF = True + +AUTO_LOAD = ["key_provider"] + +CONF_START_KEYS = "start_keys" +CONF_END_KEYS = "end_keys" +CONF_END_KEY_REQUIRED = "end_key_required" +CONF_BACK_KEYS = "back_keys" +CONF_CLEAR_KEYS = "clear_keys" +CONF_ALLOWED_KEYS = "allowed_keys" +CONF_ON_PROGRESS = "on_progress" +CONF_ON_RESULT = "on_result" + +key_collector_ns = cg.esphome_ns.namespace("key_collector") +KeyCollector = key_collector_ns.class_("KeyCollector", cg.Component) + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(KeyCollector), + cv.GenerateID(CONF_SOURCE_ID): cv.use_id(key_provider.KeyProvider), + cv.Optional(CONF_MIN_LENGTH): cv.int_, + cv.Optional(CONF_MAX_LENGTH): cv.int_, + cv.Optional(CONF_START_KEYS): cv.string, + cv.Optional(CONF_END_KEYS): cv.string, + cv.Optional(CONF_END_KEY_REQUIRED): cv.boolean, + cv.Optional(CONF_BACK_KEYS): cv.string, + cv.Optional(CONF_CLEAR_KEYS): cv.string, + cv.Optional(CONF_ALLOWED_KEYS): cv.string, + cv.Optional(CONF_ON_PROGRESS): automation.validate_automation(single=True), + cv.Optional(CONF_ON_RESULT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_TIMEOUT): automation.validate_automation(single=True), + cv.Optional(CONF_TIMEOUT): cv.positive_time_period_milliseconds, + } + ), + cv.has_at_least_one_key(CONF_END_KEYS, CONF_MAX_LENGTH), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_provider(source)) + if CONF_MIN_LENGTH in config: + cg.add(var.set_min_length(config[CONF_MIN_LENGTH])) + if CONF_MAX_LENGTH in config: + cg.add(var.set_max_length(config[CONF_MAX_LENGTH])) + if CONF_START_KEYS in config: + cg.add(var.set_start_keys(config[CONF_START_KEYS])) + if CONF_END_KEYS in config: + cg.add(var.set_end_keys(config[CONF_END_KEYS])) + if CONF_END_KEY_REQUIRED in config: + cg.add(var.set_end_key_required(config[CONF_END_KEY_REQUIRED])) + if CONF_BACK_KEYS in config: + cg.add(var.set_back_keys(config[CONF_BACK_KEYS])) + if CONF_CLEAR_KEYS in config: + cg.add(var.set_clear_keys(config[CONF_CLEAR_KEYS])) + if CONF_ALLOWED_KEYS in config: + cg.add(var.set_allowed_keys(config[CONF_ALLOWED_KEYS])) + if CONF_ON_PROGRESS in config: + await automation.build_automation( + var.get_progress_trigger(), + [(cg.std_string, "x"), (cg.uint8, "start")], + config[CONF_ON_PROGRESS], + ) + if CONF_ON_RESULT in config: + await automation.build_automation( + var.get_result_trigger(), + [(cg.std_string, "x"), (cg.uint8, "start"), (cg.uint8, "end")], + config[CONF_ON_RESULT], + ) + if CONF_ON_TIMEOUT in config: + await automation.build_automation( + var.get_timeout_trigger(), + [(cg.std_string, "x"), (cg.uint8, "start")], + config[CONF_ON_TIMEOUT], + ) + if CONF_TIMEOUT in config: + cg.add(var.set_timeout(config[CONF_TIMEOUT])) diff --git a/esphome/components/key_collector/key_collector.cpp b/esphome/components/key_collector/key_collector.cpp new file mode 100644 index 0000000000..a9213890ee --- /dev/null +++ b/esphome/components/key_collector/key_collector.cpp @@ -0,0 +1,95 @@ +#include "key_collector.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace key_collector { + +static const char *const TAG = "key_collector"; + +KeyCollector::KeyCollector() + : progress_trigger_(new Trigger()), + result_trigger_(new Trigger()), + timeout_trigger_(new Trigger()) {} + +void KeyCollector::loop() { + if ((this->timeout_ == 0) || this->result_.empty() || (millis() - this->last_key_time_ < this->timeout_)) + return; + this->timeout_trigger_->trigger(this->result_, this->start_key_); + this->clear(); +} + +void KeyCollector::dump_config() { + ESP_LOGCONFIG(TAG, "Key Collector:"); + if (this->min_length_ > 0) + ESP_LOGCONFIG(TAG, " min length: %d", this->min_length_); + if (this->max_length_ > 0) + ESP_LOGCONFIG(TAG, " max length: %d", this->max_length_); + if (!this->back_keys_.empty()) + ESP_LOGCONFIG(TAG, " erase keys '%s'", this->back_keys_.c_str()); + if (!this->clear_keys_.empty()) + ESP_LOGCONFIG(TAG, " clear keys '%s'", this->clear_keys_.c_str()); + if (!this->start_keys_.empty()) + ESP_LOGCONFIG(TAG, " start keys '%s'", this->start_keys_.c_str()); + if (!this->end_keys_.empty()) { + ESP_LOGCONFIG(TAG, " end keys '%s'", this->end_keys_.c_str()); + ESP_LOGCONFIG(TAG, " end key is required: %s", ONOFF(this->end_key_required_)); + } + if (!this->allowed_keys_.empty()) + ESP_LOGCONFIG(TAG, " allowed keys '%s'", this->allowed_keys_.c_str()); + if (this->timeout_ > 0) + ESP_LOGCONFIG(TAG, " entry timeout: %0.1f", this->timeout_ / 1000.0); +} + +void KeyCollector::set_provider(key_provider::KeyProvider *provider) { + provider->add_on_key_callback([this](uint8_t key) { this->key_pressed_(key); }); +} + +void KeyCollector::clear(bool progress_update) { + this->result_.clear(); + this->start_key_ = 0; + if (progress_update) + this->progress_trigger_->trigger(this->result_, 0); +} + +void KeyCollector::key_pressed_(uint8_t key) { + this->last_key_time_ = millis(); + if (!this->start_keys_.empty() && !this->start_key_) { + if (this->start_keys_.find(key) != std::string::npos) { + this->start_key_ = key; + this->progress_trigger_->trigger(this->result_, this->start_key_); + } + return; + } + if (this->back_keys_.find(key) != std::string::npos) { + if (!this->result_.empty()) { + this->result_.pop_back(); + this->progress_trigger_->trigger(this->result_, this->start_key_); + } + return; + } + if (this->clear_keys_.find(key) != std::string::npos) { + if (!this->result_.empty()) + this->clear(); + return; + } + if (this->end_keys_.find(key) != std::string::npos) { + if ((this->min_length_ == 0) || (this->result_.size() >= this->min_length_)) { + this->result_trigger_->trigger(this->result_, this->start_key_, key); + this->clear(); + } + return; + } + if (!this->allowed_keys_.empty() && (this->allowed_keys_.find(key) == std::string::npos)) + return; + if ((this->max_length_ == 0) || (this->result_.size() < this->max_length_)) + this->result_.push_back(key); + if ((this->max_length_ > 0) && (this->result_.size() == this->max_length_) && (!this->end_key_required_)) { + this->result_trigger_->trigger(this->result_, this->start_key_, 0); + this->clear(false); + } + this->progress_trigger_->trigger(this->result_, this->start_key_); +} + +} // namespace key_collector +} // namespace esphome diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h new file mode 100644 index 0000000000..5e63397839 --- /dev/null +++ b/esphome/components/key_collector/key_collector.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include "esphome/components/key_provider/key_provider.h" +#include "esphome/core/automation.h" + +namespace esphome { +namespace key_collector { + +class KeyCollector : public Component { + public: + KeyCollector(); + void loop() override; + void dump_config() override; + void set_provider(key_provider::KeyProvider *provider); + void set_min_length(int min_length) { this->min_length_ = min_length; }; + void set_max_length(int max_length) { this->max_length_ = max_length; }; + void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); }; + void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); }; + void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; }; + void set_back_keys(std::string back_keys) { this->back_keys_ = std::move(back_keys); }; + void set_clear_keys(std::string clear_keys) { this->clear_keys_ = std::move(clear_keys); }; + void set_allowed_keys(std::string allowed_keys) { this->allowed_keys_ = std::move(allowed_keys); }; + Trigger *get_progress_trigger() const { return this->progress_trigger_; }; + Trigger *get_result_trigger() const { return this->result_trigger_; }; + Trigger *get_timeout_trigger() const { return this->timeout_trigger_; }; + void set_timeout(int timeout) { this->timeout_ = timeout; }; + + void clear(bool progress_update = true); + + protected: + void key_pressed_(uint8_t key); + + int min_length_{0}; + int max_length_{0}; + std::string start_keys_; + std::string end_keys_; + bool end_key_required_{false}; + std::string back_keys_; + std::string clear_keys_; + std::string allowed_keys_; + std::string result_; + uint8_t start_key_{0}; + Trigger *progress_trigger_; + Trigger *result_trigger_; + Trigger *timeout_trigger_; + uint32_t last_key_time_; + uint32_t timeout_{0}; +}; + +} // namespace key_collector +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 3799e55e35..5662531a5e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -465,6 +465,7 @@ CONF_ON_TAG = "on_tag" CONF_ON_TAG_REMOVED = "on_tag_removed" CONF_ON_TIME = "on_time" CONF_ON_TIME_SYNC = "on_time_sync" +CONF_ON_TIMEOUT = "on_timeout" CONF_ON_TOUCH = "on_touch" CONF_ON_TURN_OFF = "on_turn_off" CONF_ON_TURN_ON = "on_turn_on" diff --git a/tests/test5.yaml b/tests/test5.yaml index 118b206454..23046939d5 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -168,14 +168,22 @@ binary_sensor: sn74hc165: sn74hc165_hub number: 0 - - - platform: ezo_pmp pump_state: name: "Pump State" is_paused: name: "Is Paused" + - platform: matrix_keypad + keypad_id: keypad + id: key4 + row: 1 + col: 1 + - platform: matrix_keypad + id: key1 + key: 1 + + tlc5947: data_pin: GPIO12 clock_pin: GPIO14 @@ -558,3 +566,21 @@ sn74hc165: load_pin: GPIO27 clock_inhibit_pin: GPIO26 sr_count: 4 + + +matrix_keypad: + id: keypad + rows: + - pin: 21 + - pin: 19 + columns: + - pin: 17 + - pin: 16 + keys: "1234" + +key_collector: + - id: reader + source_id: keypad + min_length: 4 + max_length: 4 +