Access control with ESPHome, Home Assistant and Node Red


I have three garage doors I needed RFID access control for. All of them are already controlled by Home Assistant using ESP8266’s and ESPHome. So what I needed was a simple RFID reader that could send tag ID to Home Assistant for processing. Home Assistant will check if the tag is valid and if so, which door to open. The tag reader is relatively cheap one from Ali Express combined with an Wemos D1 mini running ESPHome, which is natively supported in Home Assistant.

I have two different kind of readers, one simple for my workshop door, and one more complex for the dual garage doors. The latter one features two wiegand readers as seen in the video, one for each door. In addition it has two physical buttons on the inside for controlling the doors from inside. Both of the readers feature a buzzer.

The code for the simple one is here:

esphome:
  includes:
    - wiegand_device.h
  name: rfid-verksted
  platform: ESP8266
  board: d1_mini

wifi:
  ssid: "your_ssid"
  password: "your_pwd"

captive_portal:
logger:
api:
ota:

light:
  - platform: status_led
    name: "LED"
    pin:
      number: D4
      #inverted: true
    internal: true
    
sensor:
- platform: custom
  lambda: |-
    auto wiegand = new WiegandReader(D1, D2);
    App.register_component(wiegand);
    return {wiegand};
  sensors:
    name: "RFID Verksted"
    on_value:
      then:
        - homeassistant.tag_scanned: !lambda |-
            char buf[16];
            sprintf(buf, "%.0f", x);
            std::string s = buf;
            std::string y = "Verksted: " + s;
            return y;        


   

switch:
- platform: gpio
  id: "beep"
  pin: D3
  inverted: yes
  internal: true
  
- platform: template 
  name: "Denied beep Verksted"
  icon: "mdi:bell-alert"
  id: long_beep
  turn_on_action:
    - switch.turn_on: beep
    - delay: 100ms 
    - switch.turn_off: beep
    - delay: 100ms
    - switch.turn_on: beep
    - delay: 100ms
    - switch.turn_off: beep
    - delay: 100ms
    - switch.turn_on: beep
    - delay: 100ms
    - switch.turn_off: beep
    - delay: 100ms
    - switch.turn_on: beep
    - delay: 100ms
    - switch.turn_off: beep
    - switch.turn_off: long_beep

- platform: template 
  name: "Granted beep Verksted"
  icon: "mdi:bell-alert-outline" 
  id: two_beep
  turn_on_action:
    - switch.turn_on: beep
    - delay: 100ms 
    - switch.turn_off: beep
    - switch.turn_off: two_beep               

Code for the more complex one is here:

esphome:
  includes:
    - wiegand_device.h
    - wiegand_device2.h
  name: rfid-garasje
  platform: ESP8266
  board: d1_mini

wifi:
  ssid: "your_ssid"
  password: "your_pwd"

captive_portal:
logger:
api:
ota:

light:
  - platform: status_led
    name: "LED"
    pin:
      number: D4
      #inverted: true
    internal: true
    
sensor:
- platform: custom
  lambda: |-
    auto wiegand = new WiegandReader(D1, D2);
    App.register_component(wiegand);
    return {wiegand};
  sensors:
    name: "RFID Garasje Høyre"
    on_value:
      then:
        - homeassistant.tag_scanned: !lambda |-
            char buf[16];
            sprintf(buf, "%.0f", x);
            std::string s = buf;
            std::string y = "GarasjeH: " + s;
            return y;        

- platform: custom
  lambda: |-
    auto wiegand = new WiegandReader2(D7, D8);
    App.register_component(wiegand);
    return {wiegand};
  sensors:
    name: "RFID Garasje Venstre"
    on_value:
      then:
        - homeassistant.tag_scanned: !lambda |-
            char buf[16];
            sprintf(buf, "%.0f", x);
            std::string u = buf;
            std::string t = "GarasjeV: " + u;
            return t;        
   
binary_sensor:
- platform: gpio
  pin:
    number: D6
    inverted: true
    mode:
      input: true
      pullup: true
  name: RFID knapp høyre port
  filters:
    - delayed_on: 20ms
    - delayed_off: 100ms
- platform: gpio
  pin:
    number: D5
    inverted: true
    mode:
      input: true
      pullup: true
  name: RFID knapp venstre port
  filters:
    - delayed_on: 20ms      
    - delayed_off: 100ms  
switch:
- platform: gpio
  id: "beep"
  pin: D3
  inverted: yes
  internal: true
  
- platform: template 
  name: "Denied beep Garasje"
  icon: "mdi:bell-alert"
  id: long_beep
  turn_on_action:
    - switch.turn_on: beep
    - delay: 100ms 
    - switch.turn_off: beep
    - delay: 100ms
    - switch.turn_on: beep
    - delay: 100ms
    - switch.turn_off: beep
    - delay: 100ms
    - switch.turn_on: beep
    - delay: 100ms
    - switch.turn_off: beep
    - delay: 100ms
    - switch.turn_on: beep
    - delay: 100ms
    - switch.turn_off: beep
    - switch.turn_off: long_beep

- platform: template 
  name: "Granted beep Garasje"
  icon: "mdi:bell-alert-outline" 
  id: two_beep
  turn_on_action:
    - switch.turn_on: beep
    - delay: 100ms 
    - switch.turn_off: beep
    - switch.turn_off: two_beep               

Wiring is easy, just read the code. As wiegand protocol is not natively supported in ESPHome, you also need to put the file “wiegand_device.h” in the esphome folder in Home Asssistant.

When everything is set up correctly, scanned tags are sent to Home Assistant as “tag_scanned” events, and will also show up in the native Home Assistant “Tags” section. The format tags are sent in is [reader_name]: [tag_id]. This can of course easily be changed in the code to e.g. just send the tag ID.

I use node red to listen to tag_scanned events, and when discovered, it splits the response in two, checks latter part (tag_id), if approved, checks first part (which reader), sends beeps to the reader buzzer, and then opens the door. If tag is invalid a rapid series of beeps is played in the reader buzzer.

Probably forgot a lot, if anyone is interrested in building something similar, I’d be happy to help.

10 Likes

Nice work!

You might also like to check out ESP-rfid.

I’ve tested esp-rfid extensively, even made a fork of the project. It is a good project even though it seems further development is halted. Esp-rfid is meant more of a local and standalone solution than what I wanted, as my setup has a centralized controller. But for a standalone solution, I can highly recommend esp-rfid. I’ve tested it quite a lot against these wiegand readers, and it works flawlessly. Their web interface is also good.

1 Like

Hi, would you be so kind as to share the nodered flow? as i am very new to nodered and still learning it. Sorry for my English

Just use the import feature in Node Red

[{"id":"a1d87334ada573aa","type":"comment","z":"7d9b421a91dcc191","name":"Høyre garasjeport","info":"","x":1150,"y":720,"wires":[]},{"id":"819801e8beb2a1e5","type":"comment","z":"7d9b421a91dcc191","name":"Venstre garasjeport","info":"","x":1150,"y":820,"wires":[]},{"id":"5e577e0eaf7dde2d","type":"comment","z":"7d9b421a91dcc191","name":"Verkstedport","info":"","x":1130,"y":920,"wires":[]},{"id":"1aa70b922984dea1","type":"api-call-service","z":"7d9b421a91dcc191","name":"Granted garage","server":"215c2d51.4e4e8a","version":5,"debugenabled":false,"domain":"switch","service":"turn_on","areaId":[],"deviceId":[],"entityId":["switch.granted_beep_garasje"],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1150,"y":660,"wires":[[]]},{"id":"ab450f2122482ab5","type":"api-call-service","z":"7d9b421a91dcc191","name":"Denied workshop","server":"215c2d51.4e4e8a","version":5,"debugenabled":false,"domain":"switch","service":"turn_on","areaId":[],"deviceId":[],"entityId":["switch.denied_beep_verksted"],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":880,"y":1060,"wires":[[]]},{"id":"c84199578746ac1f","type":"switch","z":"7d9b421a91dcc191","name":"Hvilken leser","property":"payload.reader","propertyType":"msg","rules":[{"t":"cont","v":"GarasjeH","vt":"str"},{"t":"cont","v":"GarasjeV","vt":"str"},{"t":"cont","v":"Verksted","vt":"str"}],"checkall":"true","repair":false,"outputs":3,"x":610,"y":860,"wires":[["1aa70b922984dea1","cbf1d683bca84ab4"],["1aa70b922984dea1","1b23e6fff6ddcccc"],["c50d9d48dd7756de","973afff30b3e1e82"]]},{"id":"1c049bca3952513a","type":"server-events","z":"7d9b421a91dcc191","name":"Tag Scanned","server":"215c2d51.4e4e8a","version":2,"eventType":"tag_scanned","exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"waitForRunning":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"$outputData(\"eventData\").event_type","valueType":"jsonata"}],"x":110,"y":900,"wires":[["80ddcbf51d011db3"]]},{"id":"90725b1bc6ee69c8","type":"switch","z":"7d9b421a91dcc191","name":"Which tag?","property":"payload.tag_id","propertyType":"msg","rules":[{"t":"eq","v":"11111111","vt":"str"},{"t":"eq","v":"222222222","vt":"str"},{"t":"eq","v":"333333333","vt":"str"},{"t":"eq","v":"4444444444","vt":"str"},{"t":"nnull"}],"checkall":"false","repair":false,"outputs":5,"x":390,"y":900,"wires":[["c84199578746ac1f"],["c84199578746ac1f"],["c84199578746ac1f"],["c84199578746ac1f"],["a88db2b3b3e0fbc6"]]},{"id":"80ddcbf51d011db3","type":"function","z":"7d9b421a91dcc191","name":"tagid","func":"newmsg = {};\nnewmsg.payload = \n{\n    tag_id: msg.payload.event.tag_id.split(\" \")[1],\n    reader: msg.payload.event.tag_id.split(\":\")[0]\n}\nreturn newmsg;","outputs":1,"noerr":1,"initialize":"","finalize":"","libs":[],"x":250,"y":900,"wires":[["90725b1bc6ee69c8"]]},{"id":"6397ca67e6118b5c","type":"server-state-changed","z":"7d9b421a91dcc191","name":"button right door","server":"215c2d51.4e4e8a","version":4,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"binary_sensor.rfid_knapp_hoyre_port","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"on","halt_if_type":"str","halt_if_compare":"is","outputs":2,"output_only_on_state_change":true,"for":0,"forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":620,"y":800,"wires":[["1aa70b922984dea1","cbf1d683bca84ab4"],[]]},{"id":"422779c109653b0a","type":"server-state-changed","z":"7d9b421a91dcc191","name":"button left door","server":"215c2d51.4e4e8a","version":4,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"binary_sensor.rfid_knapp_venstre_port","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"on","halt_if_type":"str","halt_if_compare":"is","outputs":2,"output_only_on_state_change":true,"for":0,"forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":620,"y":920,"wires":[["1aa70b922984dea1","1b23e6fff6ddcccc"],[]]},{"id":"887a731b860a0661","type":"api-call-service","z":"7d9b421a91dcc191","name":"Denied garage","server":"215c2d51.4e4e8a","version":5,"debugenabled":false,"domain":"switch","service":"turn_on","areaId":[],"deviceId":[],"entityId":["switch.denied_beep_garasje"],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":860,"y":1000,"wires":[[]]},{"id":"a88db2b3b3e0fbc6","type":"switch","z":"7d9b421a91dcc191","name":"Which reader","property":"payload.reader","propertyType":"msg","rules":[{"t":"cont","v":"GarasjeH","vt":"str"},{"t":"cont","v":"GarasjeV","vt":"str"},{"t":"cont","v":"Verksted","vt":"str"}],"checkall":"true","repair":false,"outputs":3,"x":610,"y":1020,"wires":[["887a731b860a0661"],["887a731b860a0661"],["ab450f2122482ab5"]]},{"id":"c50d9d48dd7756de","type":"api-call-service","z":"7d9b421a91dcc191","name":"Granted workshop","server":"215c2d51.4e4e8a","version":5,"debugenabled":false,"domain":"switch","service":"turn_on","areaId":[],"deviceId":[],"entityId":["switch.granted_beep_verksted"],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1150,"y":1020,"wires":[[]]},{"id":"cbf1d683bca84ab4","type":"api-call-service","z":"7d9b421a91dcc191","name":"right garage door","server":"215c2d51.4e4e8a","version":5,"debugenabled":false,"domain":"switch","service":"turn_on","areaId":[],"deviceId":[],"entityId":["switch.hoyre_garasjeport"],"data":"","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1150,"y":760,"wires":[[]]},{"id":"1b23e6fff6ddcccc","type":"api-call-service","z":"7d9b421a91dcc191","name":"left garage door","server":"215c2d51.4e4e8a","version":5,"debugenabled":false,"domain":"switch","service":"turn_on","areaId":[],"deviceId":[],"entityId":["switch.venstre_garasjeport"],"data":"","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1150,"y":860,"wires":[[]]},{"id":"973afff30b3e1e82","type":"api-call-service","z":"7d9b421a91dcc191","name":"workshop door","server":"215c2d51.4e4e8a","version":5,"debugenabled":false,"domain":"switch","service":"turn_on","areaId":[],"deviceId":[],"entityId":["switch.verkstedport"],"data":"","dataType":"json","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1140,"y":960,"wires":[[]]},{"id":"215c2d51.4e4e8a","type":"server","name":"Home Assistant","version":5,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true,"heartbeat":false,"heartbeatInterval":"30","areaSelector":"friendlyName","deviceSelector":"id","entitySelector":"id","statusSeparator":"at: ","statusYear":"hidden","statusMonth":"short","statusDay":"numeric","statusHourCycle":"h23","statusTimeFormat":"h:m","enableGlobalContextStore":true}]



Hi im getting this error when trying to install your smaller version of code on the d1 mini. I dont know much about coding i barely get by on some of the harder stuff with hours of googling lol

function:
/config/esphome/rfid-reader.yaml:41:26: error: expected type-specifier before ‘WiegandReader’
41 | auto wiegand = new WiegandReader(GPIO5, GPIO4);
| ^~~~~~~~~~~~~
/config/esphome/rfid-reader.yaml:43:22: error: could not convert ‘{wiegand}’ from ‘’ to ‘std::vectoresphome::sensor::Sensor*
43 | return {wiegand};
| ^
| |
|
*** [/data/rfid-reader/.pioenvs/rfid-reader/src/main.cpp.o] Error 1
========================== [FAILED] Took 2.60 seconds ==========================

Did you put the file wiegand_device.h in the esphome folder. I might have forgot to mention you need it. Just Google the file and you’ll find it on GitHub.

I thought i had put it in the correct location but upon reading the github it appears i didnt but when i did put it in where the directions instructed i appear to be getting the same error. This is my first foray with esphome, i really appreciate your help. Is it possible you could detail where in the esphome folder it goes?

ok scratch the last response, ive made it farther into the install. i was missing the includes:

  • wiegand_device.h
    in the esphome yaml file

but now im getting another error lol

In file included from src/main.cpp:37:
src/wiegand_device.h:24:72: error: expected class-name before ‘{’ token
24 | class WiegandReader : public PollingComponent, public CustomMQTTDevice {
| ^
src/wiegand_device.h: In member function ‘void WiegandReader::json_message(std::string)’:
src/wiegand_device.h:89:13: error: ‘RealTimeClock’ was not declared in this scope
89 | RealTimeClock *x = new RealTimeClock();
| ^~~~~~~~~~~~~
src/wiegand_device.h:89:28: error: ‘x’ was not declared in this scope
89 | RealTimeClock x = new RealTimeClock();
| ^
src/wiegand_device.h:89:36: error: expected type-specifier before ‘RealTimeClock’
89 | RealTimeClock x = new RealTimeClock();
| ^~~~~~~~~~~~~
src/wiegand_device.h:90:13: error: ‘ESPTime’ was not declared in this scope
90 | ESPTime time = x->utcnow();
| ^~~~~~~
src/wiegand_device.h:92:18: error: request for member ‘strftime’ in ‘time’, which is of non-class type 'time_t(time_t
)’ {aka 'long long int(long long int
)‘}
92 | time.strftime(time2, 20, “%Y-%m-%d %H:%M:%S”);
| ^~~~~~~~
src/wiegand_device.h:94:37: error: ‘JsonObject’ has not been declared
94 | publish_json(topic, [=](JsonObject &root2) {
| ^~~~~~~~~~
src/wiegand_device.h: In lambda function:
src/wiegand_device.h:95:31: error: assignment of read-only location ‘“door”[root2]’
95 | root2[“door”] = doorNumber;
| ^~
src/wiegand_device.h:96:31: error: assignment of read-only location ‘“code”[root2]’
96 | root2[“code”] = keyCode.c_str();
| ^

src/wiegand_device.h:97:36: error: assignment of read-only location ‘“timeStamp”[root2]’
97 | root2[“timeStamp”] = time2;
| ~^
src/wiegand_device.h: In member function ‘void WiegandReader::json_message(std::string)’:
src/wiegand_device.h:94:13: error: ‘publish_json’ was not declared in this scope
94 | publish_json(topic, [=](JsonObject &root2) {
| ^
~
src/wiegand_device.h: In member function ‘void WiegandReader::json_message2(long unsigned int)’:
src/wiegand_device.h:102:13: error: ‘RealTimeClock’ was not declared in this scope
102 | RealTimeClock *x = new RealTimeClock();
| ^
~~~~~~~~
src/wiegand_device.h:102:28: error: ‘x’ was not declared in this scope
102 | RealTimeClock x = new RealTimeClock();
| ^
src/wiegand_device.h:102:36: error: expected type-specifier before ‘RealTimeClock’
102 | RealTimeClock x = new RealTimeClock();
| ^~~~~~~~~~~~~
src/wiegand_device.h:103:13: error: ‘ESPTime’ was not declared in this scope
103 | ESPTime time = x->utcnow();
| ^~~~~~~
src/wiegand_device.h:105:18: error: request for member ‘strftime’ in ‘time’, which is of non-class type 'time_t(time_t
)’ {aka 'long long int(long long int
)’}
105 | time.strftime(time2, 20, “%Y-%m-%d %H:%M:%S”);
| ^~~~~~~~
src/wiegand_device.h:106:37: error: ‘JsonObject’ has not been declared
106 | publish_json(topic, [=](JsonObject &root2) {
| ^~~~~~~~~~
src/wiegand_device.h: In lambda function:
src/wiegand_device.h:107:31: error: assignment of read-only location ‘“door”[root2]’
107 | root2[“door”] = doorNumber;
| ~~~~^~
src/wiegand_device.h:108:31: error: assignment of read-only location ‘“code”[root2]’
108 | root2[“code”] = valueID;
| ^
src/wiegand_device.h:109:36: error: assignment of read-only location ‘“timeStamp”[root2]’
109 | root2[“timeStamp”] = time2 ;
| ~^
src/wiegand_device.h: In member function ‘void WiegandReader::json_message2(long unsigned int)’:
src/wiegand_device.h:106:13: error: ‘publish_json’ was not declared in this scope
106 | publish_json(topic, [=](JsonObject &root2) {
| ^
~
/config/esphome/rfid-reader.yaml: In lambda function:
/config/esphome/rfid-reader.yaml:43:40: error: ‘GPIO5’ was not declared in this scope
43 | auto wiegand = new WiegandReader(GPIO5, GPIO4);
| ^

/config/esphome/rfid-reader.yaml:43:47: error: ‘GPIO4’ was not declared in this scope
43 | auto wiegand = new WiegandReader(GPIO5, GPIO4);
| ^

/config/esphome/rfid-reader.yaml:45:22: error: could not convert ‘{wiegand}’ from ‘’ to ‘std::vectoresphome::sensor::Sensor*
45 | return {wiegand};
| ^
| |
|
Compiling /data/rfid-reader/.pioenvs/rfid-reader/lib67b/ESP8266WiFi/WiFiClient.cpp.o
*** [/data/rfid-reader/.pioenvs/rfid-reader/src/main.cpp.o] Error 1
========================== [FAILED] Took 3.20 seconds ==========================

Hard to tell. Did you change the code?
Are you sure you have the correct library? I’ve pasted the one I use.
wiegand_device.h.zip (2.8 KB)

Also you should use blockquote to paste code here, makes it much easier to read.

1 Like

Ok so your code must be a bit different because it seems to be almost working, i got rid of two errors for I_CACHE_RAM_ATTR as it wanted just IRAM_ATTR. But i do get it looks like two other errors related to D1 and D2. I am sorry as i am replying from my phone so i dont know how to blockqoute this, again i really appreciate your help.

/config/esphome/rfid-reader.yaml: In lambda function:
/config/esphome/rfid-reader.yaml:43:40: error: ‘D1’ was not declared in this scope; did you mean ‘y1’?
43 | auto wiegand = new WiegandReader(D1 , D2);
| ^~
| y1
/config/esphome/rfid-reader.yaml:43:45: error: ‘D2’ was not declared in this scope
43 | auto wiegand = new WiegandReader(D1 , D2);
| ^~
/config/esphome/rfid-reader.yaml:45:22: error: could not convert ‘{wiegand}’ from ‘’ to ‘std::vectoresphome::sensor::Sensor*
45 | return {wiegand};
| ^
| |
|
Compiling /data/rfid-reader/.pioenvs/rfid-reader/FrameworkArduino/core_esp8266_non32xfer.cpp.o
Compiling /data/rfid-reader/.pioenvs/rfid-reader/FrameworkArduino/core_esp8266_noniso.cpp.o
Compiling /data/rfid-reader/.pioenvs/rfid-reader/FrameworkArduino/core_esp8266_phy.cpp.o
*** [/data/rfid-reader/.pioenvs/rfid-reader/src/main.cpp.o] Error 1

Did you make any significant change to the code? Sure there was no formatting errors if you copy/pasted it? Indentation correct?

No other than the previous error about I_CACHE_RAM_ATTR, i had to change to IRAM_ATTR in the wiegand_device.h file to clear that error. After that i got the error i have now. As far as the code in esphome yaml its a direct copy of yours minus the device naming and my network info lol. Neither homeassistant or esphome flagged any code errors prior to running the install command.

As of ESPhome 2023.2.0, Wiegand support is built in, meaning you should no longer need the header file. Just add something like this to your ESPhome device’s YAML file:

wiegand:
  - id: front_door_rfid_reader
    d0: GPIO5
    d1: GPIO4
    on_tag:
      - lambda: ESP_LOGI("TAG", "received tag %s", x.c_str());

For more information, here is the ESPhome documentation for the Wiegand component.

Edit: I tried this with a HID ProxPoint Plus 6005, which is quite cheap on eBay and has fantastic read range with my NExT, and it works like a charm. You’ll just need a level shifter to convert the 5V data signals from the reader to 3.3V logic level expected by the GPIO pins on your ESP board, assuming the board uses 3.3V logic. Plus, since this style of HID reader uses very little power and can run on 5V, I can power the ESP board from a single USB cable and power the reader from the 5V pin on the ESP.