From e35cab018a464c0d39fe7916a2b5de9d7c9487e7 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 16 Jan 2024 02:05:13 -0600 Subject: [PATCH] Add NFC binary sensor platform (#6068) --- CODEOWNERS | 2 +- esphome/components/nfc/__init__.py | 5 +- .../components/nfc/binary_sensor/__init__.py | 72 +++++++++++ .../nfc/binary_sensor/binary_sensor.cpp | 114 ++++++++++++++++++ .../nfc/binary_sensor/binary_sensor.h | 38 ++++++ esphome/components/nfc/nfc.h | 14 +++ esphome/components/pn7150/__init__.py | 2 +- esphome/components/pn7150/pn7150.cpp | 6 + esphome/components/pn7150/pn7150.h | 2 +- esphome/components/pn7160/__init__.py | 2 +- esphome/components/pn7160/pn7160.cpp | 6 + esphome/components/pn7160/pn7160.h | 2 +- tests/test1.yaml | 13 ++ 13 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 esphome/components/nfc/binary_sensor/__init__.py create mode 100644 esphome/components/nfc/binary_sensor/binary_sensor.cpp create mode 100644 esphome/components/nfc/binary_sensor/binary_sensor.h diff --git a/CODEOWNERS b/CODEOWNERS index 0ff5ce4508..c497a82eab 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -227,7 +227,7 @@ esphome/components/nextion/binary_sensor/* @senexcrenshaw esphome/components/nextion/sensor/* @senexcrenshaw esphome/components/nextion/switch/* @senexcrenshaw esphome/components/nextion/text_sensor/* @senexcrenshaw -esphome/components/nfc/* @jesserockz +esphome/components/nfc/* @jesserockz @kbx81 esphome/components/noblex/* @AGalfra esphome/components/number/* @esphome/core esphome/components/ota/* @esphome/core diff --git a/esphome/components/nfc/__init__.py b/esphome/components/nfc/__init__.py index c3bbc50bf9..eea1a47b24 100644 --- a/esphome/components/nfc/__init__.py +++ b/esphome/components/nfc/__init__.py @@ -1,12 +1,13 @@ from esphome import automation import esphome.codegen as cg -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@kbx81"] nfc_ns = cg.esphome_ns.namespace("nfc") +Nfcc = nfc_ns.class_("Nfcc") NfcTag = nfc_ns.class_("NfcTag") - +NfcTagListener = nfc_ns.class_("NfcTagListener") NfcOnTagTrigger = nfc_ns.class_( "NfcOnTagTrigger", automation.Trigger.template(cg.std_string, NfcTag) ) diff --git a/esphome/components/nfc/binary_sensor/__init__.py b/esphome/components/nfc/binary_sensor/__init__.py new file mode 100644 index 0000000000..21c8298ea8 --- /dev/null +++ b/esphome/components/nfc/binary_sensor/__init__.py @@ -0,0 +1,72 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_UID +from esphome.core import HexInt +from .. import nfc_ns, Nfcc, NfcTagListener + +DEPENDENCIES = ["nfc"] + +CONF_NDEF_CONTAINS = "ndef_contains" +CONF_NFCC_ID = "nfcc_id" +CONF_TAG_ID = "tag_id" + +NfcTagBinarySensor = nfc_ns.class_( + "NfcTagBinarySensor", + binary_sensor.BinarySensor, + cg.Component, + NfcTagListener, + cg.Parented.template(Nfcc), +) + + +def validate_uid(value): + value = cv.string_strict(value) + for x in value.split("-"): + if len(x) != 2: + raise cv.Invalid( + "Each part (separated by '-') of the UID must be two characters " + "long." + ) + try: + x = int(x, 16) + except ValueError as err: + raise cv.Invalid( + "Valid characters for parts of a UID are 0123456789ABCDEF." + ) from err + if x < 0 or x > 255: + raise cv.Invalid( + "Valid values for UID parts (separated by '-') are 00 to FF" + ) + return value + + +CONFIG_SCHEMA = cv.All( + binary_sensor.binary_sensor_schema(NfcTagBinarySensor) + .extend( + { + cv.GenerateID(CONF_NFCC_ID): cv.use_id(Nfcc), + cv.Optional(CONF_NDEF_CONTAINS): cv.string, + cv.Optional(CONF_TAG_ID): cv.string, + cv.Optional(CONF_UID): validate_uid, + } + ) + .extend(cv.COMPONENT_SCHEMA), + cv.has_exactly_one_key(CONF_NDEF_CONTAINS, CONF_TAG_ID, CONF_UID), +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_NFCC_ID]) + + hub = await cg.get_variable(config[CONF_NFCC_ID]) + cg.add(hub.register_listener(var)) + if CONF_NDEF_CONTAINS in config: + cg.add(var.set_ndef_match_string(config[CONF_NDEF_CONTAINS])) + if CONF_TAG_ID in config: + cg.add(var.set_tag_name(config[CONF_TAG_ID])) + elif CONF_UID in config: + addr = [HexInt(int(x, 16)) for x in config[CONF_UID].split("-")] + cg.add(var.set_uid(addr)) diff --git a/esphome/components/nfc/binary_sensor/binary_sensor.cpp b/esphome/components/nfc/binary_sensor/binary_sensor.cpp new file mode 100644 index 0000000000..8f1f6acd51 --- /dev/null +++ b/esphome/components/nfc/binary_sensor/binary_sensor.cpp @@ -0,0 +1,114 @@ +#include "binary_sensor.h" +#include "../nfc_helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nfc { + +static const char *const TAG = "nfc.binary_sensor"; + +void NfcTagBinarySensor::setup() { + this->parent_->register_listener(this); + this->publish_initial_state(false); +} + +void NfcTagBinarySensor::dump_config() { + std::string match_str = "name"; + + LOG_BINARY_SENSOR("", "NFC Tag Binary Sensor", this); + if (!this->match_string_.empty()) { + if (!this->match_tag_name_) { + match_str = "contains"; + } + ESP_LOGCONFIG(TAG, " Tag %s: %s", match_str.c_str(), this->match_string_.c_str()); + return; + } + if (!this->uid_.empty()) { + ESP_LOGCONFIG(TAG, " Tag UID: %s", format_bytes(this->uid_).c_str()); + } +} + +void NfcTagBinarySensor::set_ndef_match_string(const std::string &str) { + this->match_string_ = str; + this->match_tag_name_ = false; +} + +void NfcTagBinarySensor::set_tag_name(const std::string &str) { + this->match_string_ = str; + this->match_tag_name_ = true; +} + +void NfcTagBinarySensor::set_uid(const std::vector &uid) { this->uid_ = uid; } + +bool NfcTagBinarySensor::tag_match_ndef_string(const std::shared_ptr &msg) { + for (const auto &record : msg->get_records()) { + if (record->get_payload().find(this->match_string_) != std::string::npos) { + return true; + } + } + return false; +} + +bool NfcTagBinarySensor::tag_match_tag_name(const std::shared_ptr &msg) { + for (const auto &record : msg->get_records()) { + if (record->get_payload().find(HA_TAG_ID_PREFIX) != std::string::npos) { + auto rec_substr = record->get_payload().substr(sizeof(HA_TAG_ID_PREFIX) - 1); + if (rec_substr.find(this->match_string_) != std::string::npos) { + return true; + } + } + } + return false; +} + +bool NfcTagBinarySensor::tag_match_uid(const std::vector &data) { + if (data.size() != this->uid_.size()) { + return false; + } + + for (size_t i = 0; i < data.size(); i++) { + if (data[i] != this->uid_[i]) { + return false; + } + } + return true; +} + +void NfcTagBinarySensor::tag_off(NfcTag &tag) { + if (!this->match_string_.empty() && tag.has_ndef_message()) { + if (this->match_tag_name_) { + if (this->tag_match_tag_name(tag.get_ndef_message())) { + this->publish_state(false); + } + } else { + if (this->tag_match_ndef_string(tag.get_ndef_message())) { + this->publish_state(false); + } + } + return; + } + if (!this->uid_.empty() && this->tag_match_uid(tag.get_uid())) { + this->publish_state(false); + } +} + +void NfcTagBinarySensor::tag_on(NfcTag &tag) { + if (!this->match_string_.empty() && tag.has_ndef_message()) { + if (this->match_tag_name_) { + if (this->tag_match_tag_name(tag.get_ndef_message())) { + this->publish_state(true); + } + } else { + if (this->tag_match_ndef_string(tag.get_ndef_message())) { + this->publish_state(true); + } + } + return; + } + if (!this->uid_.empty() && this->tag_match_uid(tag.get_uid())) { + this->publish_state(true); + } +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/binary_sensor/binary_sensor.h b/esphome/components/nfc/binary_sensor/binary_sensor.h new file mode 100644 index 0000000000..cc313c2f2b --- /dev/null +++ b/esphome/components/nfc/binary_sensor/binary_sensor.h @@ -0,0 +1,38 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/nfc/nfc.h" +#include "esphome/components/nfc/nfc_tag.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace nfc { + +class NfcTagBinarySensor : public binary_sensor::BinarySensor, + public Component, + public NfcTagListener, + public Parented { + public: + void setup() override; + void dump_config() override; + + void set_ndef_match_string(const std::string &str); + void set_tag_name(const std::string &str); + void set_uid(const std::vector &uid); + + bool tag_match_ndef_string(const std::shared_ptr &msg); + bool tag_match_tag_name(const std::shared_ptr &msg); + bool tag_match_uid(const std::vector &data); + + void tag_off(NfcTag &tag) override; + void tag_on(NfcTag &tag) override; + + protected: + bool match_tag_name_{false}; + std::string match_string_; + std::vector uid_; +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index d4d66f970f..23bfdd8ef0 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -66,5 +66,19 @@ bool mifare_classic_is_trailer_block(uint8_t block_num); uint32_t get_mifare_ultralight_buffer_size(uint32_t message_length); +class NfcTagListener { + public: + virtual void tag_off(NfcTag &tag) {} + virtual void tag_on(NfcTag &tag) {} +}; + +class Nfcc { + public: + void register_listener(NfcTagListener *listener) { this->tag_listeners_.push_back(listener); } + + protected: + std::vector tag_listeners_; +}; + } // namespace nfc } // namespace esphome diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py index 3b80b574e9..a136028011 100644 --- a/esphome/components/pn7150/__init__.py +++ b/esphome/components/pn7150/__init__.py @@ -34,7 +34,7 @@ CONF_TAG_TTL = "tag_ttl" CONF_VEN_PIN = "ven_pin" pn7150_ns = cg.esphome_ns.namespace("pn7150") -PN7150 = pn7150_ns.class_("PN7150", cg.Component) +PN7150 = pn7150_ns.class_("PN7150", nfc.Nfcc, cg.Component) EmulationOffAction = pn7150_ns.class_("EmulationOffAction", automation.Action) EmulationOnAction = pn7150_ns.class_("EmulationOnAction", automation.Action) diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index 6703ab6a12..be4d6c1bb7 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -566,6 +566,9 @@ void PN7150::erase_tag_(const uint8_t tag_index) { for (auto *trigger : this->triggers_ontagremoved_) { trigger->process(this->discovered_endpoint_[tag_index].tag); } + for (auto *listener : this->tag_listeners_) { + listener->tag_off(*this->discovered_endpoint_[tag_index].tag); + } ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); } @@ -881,6 +884,9 @@ void PN7150::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoi for (auto *trigger : this->triggers_ontag_) { trigger->process(working_endpoint.tag); } + for (auto *listener : this->tag_listeners_) { + listener->tag_on(*working_endpoint.tag); + } working_endpoint.trig_called = true; break; } diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index 4aad4e1720..54038f5085 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -142,7 +142,7 @@ struct DiscoveredEndpoint { bool trig_called; }; -class PN7150 : public Component { +class PN7150 : public nfc::Nfcc, public Component { public: void setup() override; void dump_config() override; diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py index c91ca78b03..1639041b9e 100644 --- a/esphome/components/pn7160/__init__.py +++ b/esphome/components/pn7160/__init__.py @@ -36,7 +36,7 @@ CONF_VEN_PIN = "ven_pin" CONF_WKUP_REQ_PIN = "wkup_req_pin" pn7160_ns = cg.esphome_ns.namespace("pn7160") -PN7160 = pn7160_ns.class_("PN7160", cg.Component) +PN7160 = pn7160_ns.class_("PN7160", nfc.Nfcc, cg.Component) EmulationOffAction = pn7160_ns.class_("EmulationOffAction", automation.Action) EmulationOnAction = pn7160_ns.class_("EmulationOnAction", automation.Action) diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index ce5374d1d1..a7d3b38fb7 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -591,6 +591,9 @@ void PN7160::erase_tag_(const uint8_t tag_index) { for (auto *trigger : this->triggers_ontagremoved_) { trigger->process(this->discovered_endpoint_[tag_index].tag); } + for (auto *listener : this->tag_listeners_) { + listener->tag_off(*this->discovered_endpoint_[tag_index].tag); + } ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); } @@ -905,6 +908,9 @@ void PN7160::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoi for (auto *trigger : this->triggers_ontag_) { trigger->process(working_endpoint.tag); } + for (auto *listener : this->tag_listeners_) { + listener->tag_on(*working_endpoint.tag); + } working_endpoint.trig_called = true; break; } diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index 2b3cb99453..f2e05ea1d0 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -157,7 +157,7 @@ struct DiscoveredEndpoint { bool trig_called; }; -class PN7160 : public Component { +class PN7160 : public nfc::Nfcc, public Component { public: void setup() override; void dump_config() override; diff --git a/tests/test1.yaml b/tests/test1.yaml index 3ca6faca8a..471e2a71a5 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2028,6 +2028,18 @@ binary_sensor: - platform: dfrobot_sen0395 id: mmwave_detected_uart dfrobot_sen0395_id: mmwave + - platform: nfc + nfcc_id: nfcc_pn7160_i2c + ndef_contains: pulse + name: MFC Tag 1 + - platform: nfc + nfcc_id: nfcc_pn7160_i2c + tag_id: pulse + name: MFC Tag 2 + - platform: nfc + nfcc_id: nfcc_pn7160_i2c + uid: 59-FC-AB-15 + name: MFC Tag 3 pca9685: frequency: 500 @@ -3453,6 +3465,7 @@ pn532_i2c: i2c_id: i2c_bus pn7150_i2c: + id: nfcc_pn7150_i2c i2c_id: i2c_bus irq_pin: allow_other_uses: true