diff --git a/CODEOWNERS b/CODEOWNERS index 57108f32b3..cdcf1ae2d0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -277,6 +277,7 @@ esphome/components/wake_on_lan/* @willwill2will54 esphome/components/web_server_base/* @OttoWinter esphome/components/whirlpool/* @glmnet esphome/components/whynter/* @aeonsablaze +esphome/components/wiegand/* @ssieb esphome/components/wl_134/* @hobbypunk90 esphome/components/x9c/* @EtienneMD esphome/components/xiaomi_lywsd03mmc/* @ahpohl diff --git a/esphome/components/wiegand/__init__.py b/esphome/components/wiegand/__init__.py new file mode 100644 index 0000000000..7b05c43198 --- /dev/null +++ b/esphome/components/wiegand/__init__.py @@ -0,0 +1,78 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins, automation +from esphome.components import key_provider +from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID + +CODEOWNERS = ["@ssieb"] + +AUTO_LOAD = ["key_provider"] + +MULTI_CONF = True + +wiegand_ns = cg.esphome_ns.namespace("wiegand") + +Wiegand = wiegand_ns.class_("Wiegand", key_provider.KeyProvider, cg.Component) +WiegandTagTrigger = wiegand_ns.class_( + "WiegandTagTrigger", automation.Trigger.template(cg.std_string) +) +WiegandRawTrigger = wiegand_ns.class_( + "WiegandRawTrigger", automation.Trigger.template(cg.uint8, cg.uint64) +) +WiegandKeyTrigger = wiegand_ns.class_( + "WiegandKeyTrigger", automation.Trigger.template(cg.uint8) +) + +CONF_D0 = "d0" +CONF_D1 = "d1" +CONF_ON_KEY = "on_key" +CONF_ON_RAW = "on_raw" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(Wiegand), + cv.Required(CONF_D0): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_D1): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WiegandTagTrigger), + } + ), + cv.Optional(CONF_ON_RAW): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WiegandRawTrigger), + } + ), + cv.Optional(CONF_ON_KEY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WiegandKeyTrigger), + } + ), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + pin = await cg.gpio_pin_expression(config[CONF_D0]) + cg.add(var.set_d0_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_D1]) + cg.add(var.set_d1_pin(pin)) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_tag_trigger(trigger)) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_RAW, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_raw_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.uint8, "bits"), (cg.uint64, "value")], conf + ) + + for conf in config.get(CONF_ON_KEY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_key_trigger(trigger)) + await automation.build_automation(trigger, [(cg.uint8, "x")], conf) diff --git a/esphome/components/wiegand/wiegand.cpp b/esphome/components/wiegand/wiegand.cpp new file mode 100644 index 0000000000..67558da731 --- /dev/null +++ b/esphome/components/wiegand/wiegand.cpp @@ -0,0 +1,117 @@ +#include "wiegand.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace wiegand { + +static const char *const TAG = "wiegand"; +static const char *const KEYS = "0123456789*#"; + +void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { + if (arg->d0.digital_read()) + return; + arg->count++; + arg->value <<= 1; + arg->last_bit_time = millis(); + arg->done = false; +} + +void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) { + if (arg->d1.digital_read()) + return; + arg->count++; + arg->value = (arg->value << 1) | 1; + arg->last_bit_time = millis(); + arg->done = false; +} + +void Wiegand::setup() { + this->d0_pin_->setup(); + this->store_.d0 = this->d0_pin_->to_isr(); + this->d1_pin_->setup(); + this->store_.d1 = this->d1_pin_->to_isr(); + this->d0_pin_->attach_interrupt(WiegandStore::d0_gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); + this->d1_pin_->attach_interrupt(WiegandStore::d1_gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); +} + +bool check_eparity(uint64_t value, int start, int length) { + int parity = 0; + uint64_t mask = 1LL << start; + for (int i = 0; i <= length; i++, mask <<= 1) { + if (value & i) + parity++; + } + return !(parity & 1); +} + +bool check_oparity(uint64_t value, int start, int length) { + int parity = 0; + uint64_t mask = 1LL << start; + for (int i = 0; i <= length; i++, mask <<= 1) { + if (value & i) + parity++; + } + return parity & 1; +} + +void Wiegand::loop() { + if (this->store_.done) + return; + if (millis() - this->store_.last_bit_time < 100) + return; + uint8_t count = this->store_.count; + uint64_t value = this->store_.value; + this->store_.count = 0; + this->store_.value = 0; + this->store_.done = true; + ESP_LOGV(TAG, "received %d-bit value: %llx", count, value); + for (auto *trigger : this->raw_triggers_) + trigger->trigger(count, value); + if (count == 26) { + std::string tag = to_string((value >> 1) & 0xffffff); + ESP_LOGD(TAG, "received 26-bit tag: %s", tag.c_str()); + if (!check_eparity(value, 13, 13) || !check_oparity(value, 0, 13)) { + ESP_LOGW(TAG, "invalid parity"); + return; + } + for (auto *trigger : this->tag_triggers_) + trigger->trigger(tag); + } else if (count == 34) { + std::string tag = to_string((value >> 1) & 0xffffffff); + ESP_LOGD(TAG, "received 34-bit tag: %s", tag.c_str()); + if (!check_eparity(value, 17, 17) || !check_oparity(value, 0, 17)) { + ESP_LOGW(TAG, "invalid parity"); + return; + } + for (auto *trigger : this->tag_triggers_) + trigger->trigger(tag); + } else if (count == 37) { + std::string tag = to_string((value >> 1) & 0x7ffffffff); + ESP_LOGD(TAG, "received 37-bit tag: %s", tag.c_str()); + if (!check_eparity(value, 18, 19) || !check_oparity(value, 0, 19)) { + ESP_LOGW(TAG, "invalid parity"); + return; + } + for (auto *trigger : this->tag_triggers_) + trigger->trigger(tag); + } else if (count == 4) { + for (auto *trigger : this->key_triggers_) + trigger->trigger(value); + if (value < 12) { + uint8_t key = KEYS[value]; + this->send_key_(key); + } + } else { + ESP_LOGD(TAG, "received unknown %d-bit value: %llx", count, value); + } +} + +void Wiegand::dump_config() { + ESP_LOGCONFIG(TAG, "Wiegand reader:"); + LOG_PIN(" D0 pin: ", this->d0_pin_); + LOG_PIN(" D1 pin: ", this->d1_pin_); +} + +} // namespace wiegand +} // namespace esphome diff --git a/esphome/components/wiegand/wiegand.h b/esphome/components/wiegand/wiegand.h new file mode 100644 index 0000000000..994631a3a3 --- /dev/null +++ b/esphome/components/wiegand/wiegand.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/components/key_provider/key_provider.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace wiegand { + +class Wiegand; + +struct WiegandStore { + ISRInternalGPIOPin d0; + ISRInternalGPIOPin d1; + volatile uint64_t value{0}; + volatile uint32_t last_bit_time{0}; + volatile bool done{true}; + volatile uint8_t count{0}; + + static void d0_gpio_intr(WiegandStore *arg); + static void d1_gpio_intr(WiegandStore *arg); +}; + +class WiegandTagTrigger : public Trigger {}; + +class WiegandRawTrigger : public Trigger {}; + +class WiegandKeyTrigger : public Trigger {}; + +class Wiegand : public key_provider::KeyProvider, public Component { + public: + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void setup() override; + void loop() override; + void dump_config() override; + + void set_d0_pin(InternalGPIOPin *pin) { this->d0_pin_ = pin; }; + void set_d1_pin(InternalGPIOPin *pin) { this->d1_pin_ = pin; }; + void register_tag_trigger(WiegandTagTrigger *trig) { this->tag_triggers_.push_back(trig); } + void register_raw_trigger(WiegandRawTrigger *trig) { this->raw_triggers_.push_back(trig); } + void register_key_trigger(WiegandKeyTrigger *trig) { this->key_triggers_.push_back(trig); } + + protected: + InternalGPIOPin *d0_pin_; + InternalGPIOPin *d1_pin_; + WiegandStore store_{}; + std::vector tag_triggers_; + std::vector raw_triggers_; + std::vector key_triggers_; +}; + +} // namespace wiegand +} // namespace esphome