Note: staff-provided content does not represent an official statement from FARGOS Development, LLC. The policy on staff-authored content can be found here.


Go back to Geoff's home page.

PN5180 Tag Reader and ESPHome Integration

The ultimate result of this project is a door entry system that allows users to scan key fobs to unlock a door. This project adds support for PN5180 NFC tag readers to ESPHome as a local component. Although unrelated, it also adds support for 433 MHz receivers as a second component.

Deployed NFC Reader
Deployed NFC Reader

ESPHome Integration

A complete example YAML for using the PN5180 and 433 MHz integrations appears below. Two pre-existing libraries are referenced:

The integration into ESPHome is enabled via external_components. The example below roots these components under the directory path my_components:

external_components:
  - source:
      type: local
      path: my_components
    

The example makes provision for 6 indicator LEDs:

Status
Tied to the onboard LED. Used to flash a warning indication.
Heartbeat
Blinks to indicate the system is operational
Tag Detected
Lights when an NFC tag is detected by the system.
Tag Read
Lights when an NFC tag was successfully read.
Tag Accepted
Lights when ESPHome system sees tag as authorized.
Message Received
Blinks when a message is successfully received by the 433 MHz receiver.

In practice, a user approaches the tag reader with their key fob and looks for the NFC Tag Detected LED to illuminate. The key fob needs to be held in place until the Tag Read LED is illuminated; it can be removed once the Tag Read LED has indicated a successful read of the NFC tag. The expectation is that the Tag Accepted LED will subsequently be illuminated and the door will be unlocked.

Note that the Tag Accepted LED is turned on under direction of the Home Assistant controlling host to which the ESPHome-based device is connected. The Home Assistant host is also responsible for determination of tag authorization and commanding the door to be unlocked. The Tag Accepted LED will not be illuminated if the Home Assistant host is inoperable or the tag is not in its list of authorized tags.

The YAML below includes an optional on_boot block that flashes the LEDs at startup to prove that they are in working order.

esphome:
  name: frontdoor-nfc
  friendly_name: Front Door NFC
  comment: Decodes Near Field Communication tags
  project:
    name: "gcc.fontdoor_nfc"
    version: "1.2.1"
  libraries:
    - pn5180_lib=https://github.com/ATrappmann/PN5180-Library.git#master
    - SPI
  on_boot:
    priority: -100.0
    then:
      - repeat:
          count: 3
          then:
            - output.turn_on: rf_msg_rcvd_led
            - delay: 1s
            - output.turn_off: rf_msg_rcvd_led
            - delay: 1s
            - output.turn_on: nfc_detected_indicator
            - delay: 1s
            - output.turn_off: nfc_detected_indicator
            - delay: 1s
            - output.turn_on: nfc_read_ok_indicator
            - delay: 1s
            - output.turn_off: nfc_read_ok_indicator
            - delay: 1s
            - switch.turn_on: tag_accepted_indicator
            - delay: 1s
            - switch.turn_off: tag_accepted_indicator
            - delay: 1s
            - output.turn_on: alive_led
            - delay: 1s
            - output.turn_off: alive_led
#            - delay: 1s
#            - light.turn_on: statusLED
#            - delay: 1s
#            - light.turn_off: statusLED

esp32:
  board: esp32doit-devkit-v1

  cpu_frequency: 160MHz
  framework:
    type: arduino # esp-idf
    sdkconfig_options:
      CONFIG_ESP32_BROWNOUT_DET: "n"
      CONFIG_ESP_TASK_WDT_TIMEOUT_S: "30"
    advanced:
      minimum_chip_revision: "3.1"
#      sram1_as_iram: true

# Enable logging
logger:
  level: debug # very_verbose

# Enable Home Assistant API
api:
  encryption:
    key: "${KEY}"

ota:
  - platform: esphome
    password: "ESPadmin"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  reboot_timeout: 60s
  power_save_mode: none # since plugged in
  min_auth_mode: WPA2

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "FrontDoorNFC"
    password: "NFCadmin"

captive_portal:

external_components:
  - source:
      type: local
      path: my_components

time:
  - platform: homeassistant
    id: system_clock
    timezone: "America/Chicago"

text_sensor:
  - platform: wifi_info
    ip_address:
      name: IP Address
      id: local_ip_address
    mac_address:
      name: WiFi MAC Address
      id: wifi_mac_address

sensor:
  - platform: wifi_signal # Reports the WiFi signal strength/RSSI in dB
    name: "WiFi Signal dB"
    id: wifi_signal_db
    update_interval: 60s
    entity_category: "diagnostic"
  - platform: copy # Reports the WiFi signal strength in %
    id: wifi_signal_strength
    source_id: wifi_signal_db
    name: "WiFi Signal Percent"
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "Signal %"
    entity_category: "diagnostic"
    device_class: ""

status_led:
  id: statusLED
  pin: GPIO2


switch:
  - platform: gpio
    pin: GPIO25
    id: tag_accepted_indicator
    name: "NFC Tag Accepted LED"
    restore_mode: ALWAYS_OFF
    on_turn_on:
      - delay: 5s
      - switch.turn_off: tag_accepted_indicator


output:
  - platform: gpio
    pin: GPIO27
    id: nfc_detected_indicator
  - platform: gpio
    pin: GPIO26
    id: nfc_read_ok_indicator
#    drive_strength: 40mA
  - platform: gpio
    pin: GPIO14
    id: alive_led
  - platform: gpio
    pin: GPIO17
    id: rf_msg_rcvd_led

# Light component used to blink liveness LED
light:
  - platform: binary
    name: "Blinking Liveness LED"
    output: alive_led
    id: blinking_liveness_led
    internal: true
  - platform: binary
    output: rf_msg_rcvd_led
    id: blinking_rf_recv_led
    internal: true
    on_turn_on:
      - delay: 2s
      - light.turn_off: blinking_rf_recv_led

# Automation to blink the LED
interval:
  - interval: 1s      # Blink every 1 second
    then:
      - light.toggle: blinking_liveness_led

ask433_receiver:
  data_pin: GPIO16
  msg_data:
    name: "Message Data"
  msg_count:
    name: "Message Count"
    on_value:
      then:
        - light.turn_on: blinking_rf_recv_led

spi:
  id: spi_bus
  clk_pin: GPIO18
  mosi_pin: GPIO23
  miso_pin: GPIO19

pn5180:
  id: nfc_reader
  spi_id: spi_bus
  data_rate: 2MHz
  spi_mode: 3 # Mode 3 (CPOL=1, CPHA=1)
#  bit_order: msb_first
#  poll_interval: 500
  cs_pin:
    number: GPIO15
    ignore_strapping_warning: false
  busy_pin:
    number: GPIO32
  reset_pin:
    number: GPIO33
  button1_pin:
    number: GPIO34
    mode:
      input: true
  tag_uid:
    name: "TAG UID"
  tag_data:
    name: "TAG DATA"
  tag_detected:
    name: "TAG Detected"
    internal: true
    on_press:
      then:
        - output.turn_on: nfc_detected_indicator
    on_release:
      then:
        - output.turn_off: nfc_detected_indicator
  tag_read_ok:
    name: "TAG Read OK"
    on_press:
      then:
        - output.turn_off: nfc_detected_indicator # save power
        - output.turn_on: nfc_read_ok_indicator
    on_release:
      then:
       - output.turn_off: nfc_read_ok_indicator

PN5180 Integration

PN5180 Door Panel Diagram
ESP32 DevKit to PN5180 Wiring Diagram
Reverse of Door Panel Module
Reverse side of Door Panel Module
Side View of Door Panel Module
Side View of Door Panel Module

ESPHome PN5180 Component Code

The source code for interfacing a PN5180 NFC Tag reader to ESPHome is illustrated below. It is comprised by two files: the __init__.py that exposes the interface to the YAML parser and the C++ pn5180_component.h header that implements the pn5180 component for ESPHome. The YAML presented above specifies a my_components directory path, which would require the two files below to be placed in the config/my_components/pn5180 subdirectory.

__init__.py for pn5180 ESPHome Component

Three elements are published to Home Assistant:

Tag Read OK
Boolean indication that a new tag as been successfully read
TAG UID
The UID of the seen tag
TAG DATA
The contents of the VCARD data parsed from the tag

import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import spi, text_sensor, binary_sensor, sensor
from esphome import pins

DEPENDENCIES = ["spi"]
AUTO_LOAD = [ "text_sensor", "binary_sensor"]

CONF_BUSY_PIN = "busy_pin"
CONF_RESET_PIN = "reset_pin"
CONF_CS_PIN = "cs_pin"
CONF_TAG_UID = "tag_uid"
CONF_TAG_DATA = "tag_data"
CONF_TAG_DETECTED = "tag_detected"
CONF_TAG_READ_OK = "tag_read_ok"
CONF_BTN1_PIN = "button1_pin"
# CONF_BTN1_STATE = "button1_pressed"

pn5180_ns = cg.esphome_ns.namespace("pn5180")
PN5180_Device = pn5180_ns.class_(
    "PN5180_Device",
    cg.PollingComponent,
    spi.SPIDevice
)

CONFIG_SCHEMA = (
    cv.Schema(
        {
            cv.GenerateID(): cv.declare_id(PN5180_Device),
            cv.Required(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
            cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
            cv.Required(CONF_TAG_UID): text_sensor.text_sensor_schema(),
            cv.Required(CONF_TAG_DATA): text_sensor.text_sensor_schema(),
            cv.Required(CONF_TAG_DETECTED): binary_sensor.binary_sensor_schema(),
            cv.Required(CONF_TAG_READ_OK): binary_sensor.binary_sensor_schema(),
            cv.Optional(CONF_CS_PIN): pins.gpio_output_pin_schema,
#            cv.Optional(CONF_BTN1_STATE): binary_sensor.binary_sensor_schema(),
            cv.Optional(CONF_BTN1_PIN): pins.gpio_input_pin_schema,
        }
    )
    .extend(cv.polling_component_schema("500ms"))
    .extend(spi.spi_device_schema(cs_pin_required=True))
)


async def to_code(config):
    var = cg.new_Pvariable(config[cv.CONF_ID])
    await cg.register_component(var, config)
    await spi.register_spi_device(var, config)

    busy = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
    cg.add(var.set_busy_pin(busy))

    reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
    cg.add(var.set_reset_pin(reset))

    tag = await text_sensor.new_text_sensor(config[CONF_TAG_UID])
    cg.add(var.set_tag_uid_sensor(tag))

    tag = await text_sensor.new_text_sensor(config[CONF_TAG_DATA])
    cg.add(var.set_tag_data_sensor(tag))

    sens = await binary_sensor.new_binary_sensor(config[CONF_TAG_DETECTED])
    cg.add(var.set_tag_detected_sensor(sens))

    sens = await binary_sensor.new_binary_sensor(config[CONF_TAG_READ_OK])
    cg.add(var.set_tag_read_ok_sensor(sens))

    if CONF_BTN1_PIN in config:
        pin = await cg.gpio_pin_expression(config[CONF_BTN1_PIN])
        cg.add(var.set_button1_pin(pin))

#    if CONF_BTN1_STATE in config:
#        sens = await binary_sensor.new_binary_sensor(config[CONF_BTN1_STATE])
#        cg.add(var.set_button1_sensor(sens))

pn5180_component.h

#pragma once

#include "esphome/components/spi/spi.h"
#include "esphome.h"
#include "PN5180.h"
#include "PN5180ISO15693.h"

using namespace esphome;

namespace esphome {
namespace pn5180 {

static const char hexChars[16] = {
	'0', '1', '2', '3', '4', '5', '6', '7',
	'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};

/*!
 * \brief Searches for a substring within a buffer.
 *
 * \param source is a pointer to the buffer.
 * \param srcLen indicates the length of the \a source buffer.
 * \param subString is a pointer to the string to be found.
 * \param subLen indicates the length of the \a substring.
 *
 * \return ~0 is returned if the substring is not found; otherwise the
 * offset of the first instance on the substring within the \a source
 * buffer is returned.
 */
static ssize_t findSubstring(const unsigned char *source, size_t srcLen,
                      const unsigned char *subString, size_t subLen)
{
    /* NOTE: as a practical matter, subLen is not going to be more than 4 gigs,
     * so we should be able to get away with a mere 32-bit skip list
     */
    enum { MAX_CHARS_IN_A_BYTE = 256 };
    uint32_t skip[MAX_CHARS_IN_A_BYTE];
    unsigned char c;

    if ((srcLen == 0) || (subLen == 0)) { // nothing to look at/for
        return (-1);
    }
    if (srcLen < subLen) { // impossible...
        return (-1);
    }
    if (srcLen == subLen) { // either they're equal or not...
        if (memcmp(source, subString, subLen) == 0) {
            return (0);
        }
        return (-1);
    }

    // look for first character using optimized function
    const unsigned char *ucp = reinterpret_cast<const unsigned char *>(memchr(source, *subString, srcLen));
    if (ucp == nullptr) {
        return (-1); // we'll never find anything
    }
    size_t i = ucp - source;          // offset
    if (subLen == 1) {         // FAST PATH:  single character
        return (i);
    }
    if ((i + subLen) > srcLen) { // too near the end for any match
        return (-1); // impossible to find match
    }
    // FAST PATH:  try for a match at very first occurrence
    if (memcmp(source + i, subString, subLen) == 0) { // found it!
        return (i);
    }
    // Go find it using more complex search

    // start at end of substring and work backwards towards front
    // when comparing.
    // initialize skip table--indicates how many ahead one can skip
    // when a mismatch occurs
    // if character doesn't occur within string, the substring can't
    // contain it, so move right for entire length of substring

    // Equivalent to: for(i=0;i<MAX_CHARS_IN_A_BYTE;i++) skip[i] = subLen;
    uint32_t *ulp = skip;
    for (uint_fast16_t j = 256; j != 0; --j) {
        // it is impractical to have a substring more than 4 gigabytes long
        *(ulp++) = static_cast<uint32_t>(subLen);
    }
    //  memcpy(skip + 2, skip, sizeof(skip) - 2 * sizeof(unsigned long));

    const unsigned char *ucp2 = subString;
    i = subLen - 1;
    for (size_t j = subLen; j > 0;) {  // for each character within the substring
        j -= 1;
        c = *(ucp2++);
        skip[c] = j;
    }
    // i = index within source, j = index within substring...
    for (size_t j = subLen; j > 0;) {
        j -= 1;
        c = *(source + i);
        if (c != *(subString + j)) {
            // mismatch...
            const uint32_t skipDistance = skip[c];
            const uint32_t charsMatched = (subLen - 1) - j;
            if (charsMatched > skipDistance) {
                i += charsMatched + 1;
            }
            else {
                i += skipDistance;
            }
            if (i >= srcLen) {
                return (-1); // past end of source
            }
            // restart at end of substring, --j occurs as part
            // of loop
            j = subLen;
        }
        else {
            if (j == 0) {
                break; // all done...
            }
            --i;               // look at next char...
        }
    }
    return (i);
}

template <enum spi::SPIBitOrder BIT_ORDER=spi::BIT_ORDER_MSB_FIRST, enum spi::SPIClockPolarity CLOCK_POLARITY=spi::CLOCK_POLARITY_LOW, enum spi::SPIClockPhase CLOCK_PHASE=spi::CLOCK_PHASE_LEADING, enum spi::SPIDataRate DATA_RATE=spi::DATA_RATE_1KHZ> class PN5180_Device_Interface :  public PollingComponent,
	public ::spi::SPIDevice<BIT_ORDER,CLOCK_POLARITY, CLOCK_PHASE, DATA_RATE> {
 protected:
  InternalGPIOPin *busy_pin_{nullptr};
  InternalGPIOPin *reset_pin_{nullptr};
  InternalGPIOPin *button1_pin_{nullptr};
  text_sensor::TextSensor *tag_uid_{nullptr};
  text_sensor::TextSensor *tag_data_{nullptr};
  binary_sensor::BinarySensor *tag_detected_{nullptr};
  binary_sensor::BinarySensor *tag_read_ok_{nullptr};

  // The PN5180 instance used for reading ISO15693 tags
  PN5180ISO15693 *pn5180_{nullptr};
 public:
  // set_cs_pin() implememted by SPIDevice()
  void set_busy_pin(InternalGPIOPin *pin) { busy_pin_ = pin; }
  void set_reset_pin(InternalGPIOPin *pin) { reset_pin_ = pin; }
  void set_button1_pin(InternalGPIOPin *pin) { button1_pin_ = pin; }
  void set_tag_uid_sensor(text_sensor::TextSensor *sensor) { tag_uid_ = sensor; }
  void set_tag_data_sensor(text_sensor::TextSensor *sensor) { tag_data_ = sensor; }

  void set_tag_detected_sensor(binary_sensor::BinarySensor *sensor) { tag_detected_ = sensor; }
  void set_tag_read_ok_sensor(binary_sensor::BinarySensor *sensor) { tag_read_ok_ = sensor; }

  const char *get_tag_uid() const { return tag_uid_->state.c_str(); }

  bool tag_uid_has_state() const { return tag_uid_->has_state(); }

  bool has_state() const { return tag_uid_has_state(); }

  // Setup function, called once during initialization
  void setup() override {
     // Initialize the PN5180 instance

     uint8_t busy = (busy_pin_ != nullptr) ? busy_pin_->get_pin() : 0;
     uint8_t reset = (reset_pin_ != nullptr) ? reset_pin_->get_pin() : 0;
     ESP_LOGI("pn5180", "setup busy=%d reset=%d", busy, reset);
     uint8_t cs = 0;
     if (this->cs_ != nullptr) {
	ESP_LOGI("pn5180", "Doing cast of cs_");
     	// cs_ is protected member from SPIClient class
  	InternalGPIOPin *csPin = (InternalGPIOPin *)(this->spi::SPIClient::cs_);
	if (csPin != nullptr) {
		cs = csPin->get_pin();
		ESP_LOGI("pn5180", "Got cs pin=%d", cs);
	} else {
		ESP_LOGE("pn5180", "failed to cast cs_pin");
	}
     }
//return;
     ESP_LOGI("pn5180", "creating PN5180ISO15693");
     pn5180_ = new PN5180ISO15693(cs, busy, reset);
ESP_LOGI("pn5180", "pn5180 begin");
     pn5180_->begin();

     // Reset and set up the RF settings for the PN5180
ESP_LOGI("pn5180", "pn5180 reset");
     pn5180_->reset();
ESP_LOGI("pn5180", "pn5180 setupRF");
     pn5180_->setupRF();
ESP_LOGI("pn5180", "pn5180 setup end");

  } // end setup

  // Update function, called periodically according to the update interval
  void update() override {
    static enum { IDLE=0, SAW_TAG, READ_DATA, ERROR } read_state = IDLE;
    uint8_t uid[8];
    // Attempt to read the UID of an ISO15693 tag
    ISO15693ErrorCode rc =  pn5180_->getInventory(uid);
    esp_task_wdt_reset(); // we made forward progress
    if (rc != ISO15693_EC_OK) {
      // If reading the UID fails, log an error and update the text sensor to show no tag detected
      if (read_state != IDLE) {
      	read_state = IDLE;
        ESP_LOGI("pn5180", "No NFC detected");
        tag_uid_->publish_state("NOTAVAIL");
	if (tag_detected_ != nullptr) {
		tag_detected_->publish_state(false);
	}
	if (tag_read_ok_ != nullptr) {
		tag_read_ok_->publish_state(false);
	}
      } // end if not in idle state
      return;
    }
    // tag seen
    if (read_state == READ_DATA) {
	    return; // already published data
    }
    read_state = SAW_TAG;
    if (tag_detected_ != nullptr) {
	tag_detected_->publish_state(true);
    }
    uint8_t blockSize, numBlocks;
    rc =  pn5180_->getSystemInfo(uid, &blockSize, &numBlocks);
    esp_task_wdt_reset(); // we made forward progress
    if (rc != ISO15693_EC_OK) {
	    read_state = ERROR;
	    ESP_LOGE("pn5180", "Could not getSystemInfo");
    	    // Reset and set up the RF settings on the PN5180 for the next read
    	    if (tag_detected_ != nullptr) {
		tag_detected_->publish_state(false);
    	    }
    	    pn5180_->reset();
    	    pn5180_->setupRF();
	    return;
    }
    // Convert the UID to a string; UID received least significant byte first
    // so we convert it from end to start
    char asHexString[32];
    uint8_t offset = 0;
    for (int8_t i = sizeof(uid) - 1; i >= 0; i-=1) {
          uint8_t bits = (uid[i] >> 4) & 0xf;
          asHexString[offset++] = hexChars[bits];
          bits = uid[i] & 0xf;
          asHexString[offset++] = hexChars[bits];
	  if (i != 0) {
		  asHexString[offset++] = ':';
	  }
    }
    asHexString[offset] = '\0';

    // Print the UID to the log
    ESP_LOGI("pn5180", "Read blockSize=%d numBlocks=%d UID: %s", blockSize, numBlocks, asHexString);
    // we're using cards with blockSize=4 and numBlocks=28
    enum { MAX_DATA_SIZE = 128 }; // ultimate max would be 8192 with blockSize=32 numBlocks=256
    uint8_t cardData[MAX_DATA_SIZE];
    uint16_t dataOffset = 0;
    for(uint8_t blockNum=0; blockNum < numBlocks; blockNum += 1) {
	    rc = pn5180_->readSingleBlock(uid, blockNum, cardData + dataOffset, blockSize);
	    esp_task_wdt_reset(); // we made forward progress    
            if (rc != ISO15693_EC_OK) {
	       ESP_LOGE("pn5180", "Could not readSingleBlock num=%d", blockNum);
	       read_state = ERROR;
		// Reset and set up the RF settings on the PN5180 for the next read
    		pn5180_->reset();
    		pn5180_->setupRF();
	       return;
	    }
	    dataOffset += blockSize;
    }
    cardData[dataOffset] = '\0';
    ESP_LOGI("pn5180","Data: %s", cardData);
    uint8_t b_off = 0;
    char text_bfr[48];
    for(uint16_t o1=0; o1 < dataOffset; o1 += 1) {
	    uint8_t bits = (cardData[o1] >> 4) & 0xf;
	    text_bfr[b_off++] = hexChars[bits];
	    bits = cardData[o1] & 0xf;
	    text_bfr[b_off++] = hexChars[bits];
	    if (b_off == 32) {
		text_bfr[b_off] = '\0';
		ESP_LOGI("pn5180", "%s", text_bfr);
		b_off = 0;
	    }
    }
    if (b_off != 0) {
	text_bfr[b_off] = '\0';
	ESP_LOGI("pn5180", "%s", text_bfr);
    }

    ssize_t startOffset = findSubstring(cardData, dataOffset, (const unsigned char *) "BEGIN:VCARD\n", 12);
    if (startOffset == -1) {
	    startOffset = 0;
    } else {
	    startOffset += 12;
    }
    ssize_t endOffset = findSubstring(cardData, dataOffset, (const unsigned char *) "END:VCARD", 9);
    if (endOffset == -1) endOffset = dataOffset;
    cardData[endOffset] = '\0';

    ESP_LOGI("pn5180","Data to pub:\n%s", cardData + startOffset);
    // Update the text sensor with the UID value
    read_state = READ_DATA;
    tag_uid_->publish_state(asHexString);
    tag_data_->publish_state((const char *) cardData + startOffset);
    if (tag_read_ok_ != nullptr) {
	tag_read_ok_->publish_state(true);
   }
    // Reset and set up the RF settings on the PN5180 for the next read
    pn5180_->reset();
    pn5180_->setupRF();
  } // end update


  void dump_config() override {
#define TAG "pn5180"
	  ESP_LOGCONFIG(TAG, "PN5180 NFC Interface");
	  ESP_LOGCONFIG(TAG, "  Busy Pin = %d", busy_pin_->get_pin());
	  ESP_LOGCONFIG(TAG, "  Reset Pin = %d", reset_pin_->get_pin());
	  if (button1_pin_ != nullptr) {
	  	ESP_LOGCONFIG(TAG, "  Button1 Pin = %d", button1_pin_->get_pin());
	  } else {
	  	ESP_LOGCONFIG(TAG, "  Button1 Pin NOT SPECIFIED");
	  }
#if 0
	  this->spi::SPIDevice<BIT_ORDER,CLOCK_POLARITY, CLOCK_PHASE, DATA_RATE>::dump_config();
#endif
	  this->PollingComponent::dump_config();
#undef TAG
  } // dump_config

}; // end class PN5180_Device_Interface<>

// use typedef to define default PN5180_Device
typedef PN5180_Device_Interface<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1KHZ> PN5180_Device;

} // end namespace pn5180
} // end namespace esphome

Home Assistant Automation

This sample automation requests a door to be unlocked when one of four possible tag UIDs is seen. It also requests that a Tag Accepted LED be illuminated. Note that one cannot assume that communication is 100% reliable, so burden of turning the LED off is delegated to the remote hardware.

Home Assistant Door Entry Automation
Home Assistant Door Entry Automation

433 MHz Amplitude Shift Keying Component

Since the door panel is located outside, it provides a natural location for a 433 MHz receiver board to reside and monitor signals from devices located further out on the property. The Amplitude Shift Keying implementation comes from the RadioHead library.

__init__.py for ask433_receiver ESPHome Component

This is the __init__.py file that describes the ask433_receiver component to ESPHome. The component meta data describes three elements:

data_pin
A configuration parameter that specifies the GPIO pin upon which the signal data will be received.
msg_data
A published text sensor that holds the contents of a received message.
msg_count
a published numeric value that indicates the total number of messages received successfully.
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor, binary_sensor, sensor
from esphome import pins

AUTO_LOAD = [ "text_sensor", "binary_sensor"]

CONF_DATA_PIN = "data_pin"
CONF_MSG_DATA = "msg_data"
CONF_MSG_COUNT = "msg_count"

ask433_ns = cg.esphome_ns.namespace("ASK_433MHz")
ASK433_Device = ask433_ns.class_(
    "ASK_433MHz_Receiver",
    cg.Component
)

CONFIG_SCHEMA = (
    cv.Schema(
        {
            cv.GenerateID(): cv.declare_id(ASK433_Device),
            cv.Required(CONF_DATA_PIN): pins.gpio_input_pin_schema,
            cv.Required(CONF_MSG_DATA): text_sensor.text_sensor_schema(),
            cv.Optional(CONF_MSG_COUNT): sensor.sensor_schema()
        }
    )
#    .extend(cv.polling_component_schema("500ms"))
)


async def to_code(config):
    var = cg.new_Pvariable(config[cv.CONF_ID])
    await cg.register_component(var, config)

    pin = await cg.gpio_pin_expression(config[CONF_DATA_PIN])
    cg.add(var.set_data_pin(pin))

    msg = await text_sensor.new_text_sensor(config[CONF_MSG_DATA])
    cg.add(var.set_msg_data_sensor(msg))

    if CONF_MSG_COUNT in config:
        count = await sensor.new_sensor(config[CONF_MSG_COUNT])
        cg.add(var.set_msg_counter_sensor(count))

ask433_receiver.h

This is the C++ header file that implements the ask433_receiver component for ESPHome.

#pragma once

#include "esphome.h"
#include "RH_ASK.h"

using namespace esphome;

namespace esphome {
namespace ASK_433MHz {
#define TAG "ASK_433MHz_recv"
#define UNUSED_PIN 13


class ASK_433MHz_Receiver : public Component
{
 protected:
  InternalGPIOPin *data_pin_{nullptr};
  text_sensor::TextSensor *msg_data_{nullptr};
  sensor::Sensor *msg_count_{nullptr};
  RH_ASK	*radioDevice{nullptr};
  uint32_t	messagesReceived{0};

 public:
  // set_cs_pin() implememted by SPIDevice()
  void set_data_pin(InternalGPIOPin *pin) { data_pin_ = pin; }
  void set_msg_data_sensor(text_sensor::TextSensor *sensor) { msg_data_ = sensor; }

  void set_msg_counter_sensor(sensor::Sensor *sensor) { msg_count_ = sensor; }

  const char *get_msg_data() const { return msg_data_->state.c_str(); }

  bool msg_has_state() const { return msg_data_->has_state(); }

  bool has_state() const { return msg_has_state(); }

  // Setup function, called once during initialization
  void setup() override {
     ESP_LOGI(TAG, "setup data_pin=%d", data_pin_->get_pin());
     // Initialize the radio 
     // data rate, rxPin, txPin, transmitEnablePin
     radioDevice = new RH_ASK(2000, data_pin_->get_pin(), UNUSED_PIN, UNUSED_PIN);

     if (radioDevice->init() != 0) {
	     ESP_LOGE(TAG, "Failed to initialize radio on pin %d", data_pin_->get_pin());
	     return;
     }
     ESP_LOGI(TAG, "Initialized radio on pin %d", data_pin_->get_pin());
     radioDevice->setModeRx();
  }

  void loop() override {
    uint8_t bfr[128];
    uint8_t bfrLen = sizeof(bfr);
    bool dataReceived = radioDevice->recv(bfr, &bfrLen);
    if (dataReceived) {
	    messagesReceived += 1;
	    ESP_LOGI(TAG, "Message: %s", bfr);
	    msg_data_->publish_state((const char *) bfr);
	    msg_count_->publish_state(messagesReceived);
    }
  }

  void dump_config() override {
	  ESP_LOGCONFIG(TAG, "Amplitude Shift Keying 433 MHz Receiver Interface");
	  ESP_LOGCONFIG(TAG, "  Data Pin = %d", data_pin_->get_pin());
	  if (radioDevice == nullptr) {
		  ESP_LOGCONFIG(TAG, "  Radio Device not set");
	  } else {
		  ESP_LOGCONFIG(TAG, "  Radio Device allocated");
	  }
  }
#undef TAG

}; // end class ASK_433MHz_Receiver

} // end namespace ASK_433MHz
} // end namespace esphome