Working 433mhz sensor receiver (wh2 fineoffset)

Hello i adapted this code: https://github.com/lucsmall/BetterWH2

To work on esphome (esp8266), i converted it from polling to interrupt and did some other stuff and its working with the sensors i have.

Give it a try if you are interested!

I’m running this on a esp-01 with the receiver connected to pin 3(rx), not ideal i know but i have to use all the spare esp-01 i have for something, if you are using some other esp module change to a better pin here: attachInterrupt(3, ext_int_1, CHANGE);

I have tested this with LR45B and RX470C-V01 receivers
Best of luck!

433_sensor.yaml

esphome:
  name: 433_sensor
  platform: ESP8266
  board: esp01_1m
  includes:
    - 433_sensor.h

wifi:
  ssid: ""
  password: ""

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "433_sensor Fallback Hotspot"
    password: "tAksgOaENwUm"

captive_portal:

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:
    safe_mode: True


sensor:
- platform: custom
  lambda: |-
    auto my_sensor = new MyCustomSensor();
    App.register_component(my_sensor);
    return {my_sensor->sensor1, my_sensor->sensor2};

  sensors:
  - name: "temp"
    unit_of_measurement: C
    accuracy_decimals: 1


  - name: "humidity"
    unit_of_measurement: '%'
    accuracy_decimals: 0
    



text_sensor:
- platform: custom
  lambda: |-
    auto last_sensor_data = new Last_sensor_data();
    App.register_component(last_sensor_data);
    return {last_sensor_data};

  text_sensors:
    name: "last_sensor_data"

433_sensor.h

char have_data;
char have_data_string;
std::string my_data;
float temp;
float hum;


class MyCustomSensor : public PollingComponent {
  public:


    Sensor *sensor1 = new Sensor();
    Sensor *sensor2 = new Sensor();
//Based on code from: https://github.com/lucsmall/BetterWH2
//converted from polling to interupt and adjusted to compile with esphome by Swedude




    // 1 is indicated by 500uS pulse
    // wh2_accept from 2 = 400us to 3 = 600us
#define IS_HI_PULSE(interval)   (interval >= 350 && interval <= 750)
    // 0 is indicated by ~1500us pulse
    // wh2_accept from 7 = 1400us to 8 = 1600us
#define IS_LOW_PULSE(interval)  (interval >= 1200 && interval <= 1700)
    // worst case packet length
    // 6 bytes x 8 bits =48
#define IDLE_HAS_TIMED_OUT(interval) (interval > 1150)
    // our expected pulse should arrive after 1ms
    // we'll wh2_accept it if it arrives after
    // 4 x 200us = 800us
#define IDLE_PERIOD_DONE(interval) (interval >= 800)

#define GOT_PULSE 0x01
#define LOGIC_HI  0x02

    static void ICACHE_RAM_ATTR ext_int_1()
    {

      static uint16_t pulse;
      static byte wh2_flags;
      static byte wh2_accept_flag;
      static byte wh2_packet_state;

      static byte wh2_packet[5];
      static byte wh2_calculated_crc;
      static boolean wh2_valid;
      static unsigned long last;
      static unsigned long micros_now;
      static byte sampling_state = 0;
      static byte packet_no, bit_no, history;
      micros_now = micros();
      pulse = micros_now - last;
      last = micros_now;



      switch (sampling_state) {
        case 0: // waiting

          if (IS_HI_PULSE(pulse)) {
            wh2_flags = GOT_PULSE | LOGIC_HI;
            sampling_state = 1;

          } else if (IS_LOW_PULSE(pulse)) {
            wh2_flags = GOT_PULSE; // logic low

          } else {
            sampling_state = 0;
          }
          break;
        case 1: // observe 1ms of idle time

          if (IDLE_HAS_TIMED_OUT(pulse)) {
            sampling_state = 0;
            wh2_packet_state = 1;

          } else if (IDLE_PERIOD_DONE(pulse)) {
            sampling_state = 0;
          }
          else
          {
            sampling_state = 0;
            wh2_packet_state = 1;
          }
          break;
      }

      //----------------------------------------------------
      if (wh2_flags) {

        //----------------------------------------------------

        // acquire preamble
        if (wh2_packet_state == 1) {
          // shift history right and store new value
          history <<= 1;
          // store a 1 if required (right shift along will store a 0)
          if (wh2_flags & LOGIC_HI) {
            history |= 0x01;
          }

          // check if we have a valid start of frame
          // xxxxx110
          if ((history & B00000111) == B00000110) {
            // need to clear packet, and pulseers
            packet_no = 0;
            // start at 1 becuase only need to acquire 7 bits for first packet byte.
            bit_no = 1;
            wh2_packet[0] = wh2_packet[1] = wh2_packet[2] = wh2_packet[3] = wh2_packet[4] = 0;
            // we've acquired the preamble
            wh2_packet_state = 2;
            history = 0xFF;
          }
          wh2_accept_flag = false;
        }
        // acquire packet
        else if (wh2_packet_state == 2) {
          wh2_packet[packet_no] <<= 1;
          if (wh2_flags & LOGIC_HI) {
            wh2_packet[packet_no] |= 0x01;
          }
          bit_no ++;
          if (bit_no > 7) {
            bit_no = 0;
            packet_no ++;
          }
          if (packet_no > 4) {
            // start the sampling process from scratch
            wh2_packet_state = 1;

            wh2_accept_flag = true;
          }
        }
        else
        {
          wh2_accept_flag = false;
        }

        //---------------------------------------------

        if (wh2_accept_flag) {

          int Sensor_ID = (wh2_packet[0] << 4) + (wh2_packet[1] >> 4);
          int humidity = wh2_packet[3];
          int temperature = ((wh2_packet[1] & B00000111) << 8) + wh2_packet[2];
          // make negative
          if (wh2_packet[1] & B00001000) {
            temperature = -temperature;
          }
          uint8_t crc = 0;
          uint8_t len = 4;
          uint8_t *addr = wh2_packet;
          // Indicated changes are from reference CRC-8 function in OneWire library
          while (len--) {
            uint8_t inbyte = *addr++;
            for (uint8_t i = 8; i; i--) {
              uint8_t mix = (crc ^ inbyte) & 0x80; // changed from & 0x01
              crc <<= 1; // changed from right shift
              if (mix) crc ^= 0x31;// changed from 0x8C;
              inbyte <<= 1; // changed from right shift
            }
          }
          if (crc == wh2_packet[4])
          {
            wh2_valid = true;
          }
          else
          {
            wh2_valid = false;
          }
          /*
                Serial.print("| Sensor ID: ");
                Serial.print(Sensor_ID);
                Serial.print(" | humidity: ");
                Serial.print(humidity, DEC);//power
                Serial.print(" | temperature: ");
                Serial.print(temperature / 10.0);
                Serial.print("C | ");
                Serial.println((wh2_valid ? "OK " : "BAD" ));
          */


          if (Sensor_ID == 68 && wh2_valid == true)
          {
            extern char have_data;
            have_data = 1;
            extern float temp;
            temp = temperature / 10.0;
            extern float hum;
            hum = humidity;
          }

          extern std::string my_data;
          my_data = "| Sensor ID: ";
          my_data += to_string(Sensor_ID);
          my_data += " | humidity: ";
          my_data += to_string(humidity);
          my_data += " | temperature: ";
          my_data += to_string(temperature / 10.0);
          my_data += "C | ";
          my_data += (wh2_valid ? "OK " : "BAD" );
          extern char have_data_string;
          have_data_string = 1;


          wh2_accept_flag = false;
        }
        wh2_flags = 0x00;
      }
      //--------------------------------------------------------
    }



    MyCustomSensor() : PollingComponent(100) { }

    void setup() override 
	{
		
		
      Serial.end();//if you are using rx pin for reciever (3)
	  
      attachInterrupt(3, ext_int_1, CHANGE);
	  
	  
    }



    void update() override {
      // This is the actual sensor reading logic.
      extern std::string my_data;
      extern char have_data;
      extern float temp;
      extern float hum;

      if (have_data)
      {
        have_data = 0;

        sensor1->publish_state(temp);
        sensor2->publish_state(hum);
      }

    }
};




class Last_sensor_data : public PollingComponent, public TextSensor {
  public:

    // constructor
    Last_sensor_data() : PollingComponent(100) {}

    void setup() override {
      // This will be called by App.setup()
    }
    void update() override {
      extern std::string my_data;
      extern char have_data_string;
      if (have_data_string == 1)
      {
        publish_state(my_data);
	    have_data_string = 0;
      }

      // This will be called every "update_interval" milliseconds.
      // Publish state

    }
};



4 Likes

Thanks for sharing your project.
Could you tell us more about hardware used and how it’s all connected - I believe it would greatly help anyone who wants to use your project.

Sure
Info added

1 Like

@swedude this works great also on a Sonoff RF Bridge with bypasses RF chip. Do you have any suggestions on how to sort recieved data from several sensors?

1 Like

Sure here is a updated code that i use now:

i have updated the receive code with a low pass filter from NewRemoteSwitch

and now i receive a lot more signals and less failed crc checks
and i have made it easier to add multiple sensors
Have fun and good luck

wh2_only.yaml

esphome:
  name: wh2_only_1
  platform: ESP8266
  board: esp01_1m
  includes:
    - wh2_rx.h   
wifi:
  ssid: ""
  password: ""


captive_portal:

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:
    safe_mode: True


sensor:
- platform: custom
  lambda: |-
    auto my_sensor = new MyCustomSensor();
    App.register_component(my_sensor);
    return {my_sensor->sensor1, my_sensor->sensor2,my_sensor->sensor3,my_sensor->sensor4};

  sensors:   
  - name: "storage room temp"
    unit_of_measurement: 'C'
    accuracy_decimals: 1

  - name: "storage room humidity"
    unit_of_measurement: ' %'
    accuracy_decimals: 0      

  - name: "inside temp"
    unit_of_measurement: 'C'
    accuracy_decimals: 1

  - name: "inside humidity"
    unit_of_measurement: ' %'
    accuracy_decimals: 0      
      
    
text_sensor:
- platform: custom
  lambda: |-
    auto last_sensor_data = new Last_sensor_data();
    App.register_component(last_sensor_data);
    return {last_sensor_data};

  text_sensors:
    name: "last_sensor_data"    

wh2_rx.h

char have_data_string;
std::string my_data;
int have_data_sensor;
float temp;
float hum;

class MyCustomSensor : public PollingComponent {
  public:

    Sensor *sensor1 = new Sensor();
    Sensor *sensor2 = new Sensor();
	Sensor *sensor3 = new Sensor();
	Sensor *sensor4 = new Sensor();
	
 static void ICACHE_RAM_ATTR ext_int_1()
{	
  static unsigned long edgeTimeStamp[3] = {0, };  // Timestamp of edges
  static bool skip;

  // Filter out too short pulses. This method works as a low pass filter.  (borroved from new remote reciever)
  edgeTimeStamp[1] = edgeTimeStamp[2];
  edgeTimeStamp[2] = micros();

  if (skip) {
    skip = false;
    return;
  }

  if (edgeTimeStamp[2]-edgeTimeStamp[1] < 200) {
    // Last edge was too short.
    // Skip this edge, and the next too.
    skip = true;
    return;
  }

  unsigned int pulse = edgeTimeStamp[1] - edgeTimeStamp[0];
  edgeTimeStamp[0] = edgeTimeStamp[1];
	
//--------------------------------------------------------
    // 1 is indicated by 500uS pulse
    // wh2_accept from 2 = 400us to 3 = 600us
#define IS_HI_PULSE(interval)   (interval >= 250 && interval <= 750)
    // 0 is indicated by ~1500us pulse
    // wh2_accept from 7 = 1400us to 8 = 1600us
#define IS_LOW_PULSE(interval)  (interval >= 1200 && interval <= 1750)
    // worst case packet length
    // 6 bytes x 8 bits =48
#define IDLE_HAS_TIMED_OUT(interval) (interval > 1199)
    // our expected pulse should arrive after 1ms
    // we'll wh2_accept it if it arrives after
    // 4 x 200us = 800us
#define IDLE_PERIOD_DONE(interval) (interval >= 751)
#define GOT_PULSE 0x01
#define LOGIC_HI  0x02

      //static uint16_t pulse;
      static byte wh2_flags;
      static byte wh2_accept_flag;
      static byte wh2_packet_state;
      static byte wh2_packet[5];
      static byte wh2_calculated_crc;
      static boolean wh2_valid;
      static byte sampling_state = 0;
      static byte packet_no, bit_no, history;

      switch (sampling_state) {
        case 0: // waiting

          if (IS_HI_PULSE(pulse)) {
            wh2_flags = GOT_PULSE | LOGIC_HI;
            sampling_state = 1;

          } else if (IS_LOW_PULSE(pulse)) {
            wh2_flags = GOT_PULSE; // logic low

          } else {
            sampling_state = 0;
          }
          break;
        case 1: // observe 1ms of idle time

          if (IDLE_HAS_TIMED_OUT(pulse)) {
            sampling_state = 0;
            wh2_packet_state = 1;

          } else if (IDLE_PERIOD_DONE(pulse)) {
            sampling_state = 0;
          }
          else
          {
            sampling_state = 0;
            wh2_packet_state = 1;
          }
          break;
      }


      if (wh2_flags) {


        // acquire preamble
        if (wh2_packet_state == 1) {
          // shift history right and store new value
          history <<= 1;
          // store a 1 if required (right shift along will store a 0)
          if (wh2_flags & LOGIC_HI) {
            history |= 0x01;
          }

          // check if we have a valid start of frame
          // xxxxx110
          if ((history & B00000111) == B00000110) {
            // need to clear packet, and pulseers
            packet_no = 0;
            // start at 1 becuase only need to acquire 7 bits for first packet byte.
            bit_no = 1;
            wh2_packet[0] = wh2_packet[1] = wh2_packet[2] = wh2_packet[3] = wh2_packet[4] = 0;
            // we've acquired the preamble
            wh2_packet_state = 2;
            history = 0xFF;
          }
          wh2_accept_flag = false;
        }
        // acquire packet
        else if (wh2_packet_state == 2) {
          wh2_packet[packet_no] <<= 1;
          if (wh2_flags & LOGIC_HI) {
            wh2_packet[packet_no] |= 0x01;
          }
          bit_no ++;
          if (bit_no > 7) {
            bit_no = 0;
            packet_no ++;
          }
          if (packet_no > 4) {
            // start the sampling process from scratch
            wh2_packet_state = 1;

            wh2_accept_flag = true;
          }
        }
        else
        {
          wh2_accept_flag = false;
        }


        if (wh2_accept_flag) {

          int Sensor_ID = (wh2_packet[0] << 4) + (wh2_packet[1] >> 4);
          int humidity = wh2_packet[3];
          int temperature = ((wh2_packet[1] & B00000111) << 8) + wh2_packet[2];
          // make negative
          if (wh2_packet[1] & B00001000) {
            temperature = -temperature;
          }
          uint8_t crc = 0;
          uint8_t len = 4;
          uint8_t *addr = wh2_packet;
          // Indicated changes are from reference CRC-8 function in OneWire library
          while (len--) {
            uint8_t inbyte = *addr++;
            for (uint8_t i = 8; i; i--) {
              uint8_t mix = (crc ^ inbyte) & 0x80; // changed from & 0x01
              crc <<= 1; // changed from right shift
              if (mix) crc ^= 0x31;// changed from 0x8C;
              inbyte <<= 1; // changed from right shift
            }
          }
          if (crc == wh2_packet[4])
          {
            wh2_valid = true;
          }
          else
          {
            wh2_valid = false;
          }
          extern  int have_data_sensor;
          if (wh2_valid == true && have_data_sensor==0) //avoid change sensor data during update...
          {            
			extern  float temp;
			extern  float hum;
            temp = temperature / 10.0;            
            hum = humidity;
			have_data_sensor = Sensor_ID;
          }
		  
	extern char have_data_string;	  
if (have_data_string==0) //avoid change sensor data during update.
{
          extern std::string my_data;
          my_data = "| Sensor ID: ";
          my_data += to_string(Sensor_ID);
          my_data += " | humidity: ";
          my_data += to_string(humidity);
          my_data += " | temperature: ";
          my_data += to_string(temperature / 10.0);
          my_data += "C | ";
          my_data += (wh2_valid ? "OK " : "BAD" );
          have_data_string = 1;
}

          wh2_accept_flag = false;
        }
        wh2_flags = 0x00;
      }
      //--------------------------------------------------------


}
	
 MyCustomSensor() : PollingComponent(100) { }

    void setup() override 
	{
	Serial.end();	
	attachInterrupt(3, ext_int_1, CHANGE);			  	  
    }


    void update() override {
      // This is the actual sensor reading logic.
      extern  int have_data_sensor;
      extern  float temp;
      extern  float hum;


	        if (have_data_sensor==32)
      {     
        sensor1->publish_state(temp);
        sensor2->publish_state(hum);
		have_data_sensor = 0;
      }


	        else if (have_data_sensor==68)
      {     
        sensor3->publish_state(temp);
        sensor4->publish_state(hum);
		have_data_sensor = 0;
      }		  
	  
	  	       else if (have_data_sensor)
      {
        have_data_sensor = 0;
      }

    }
};



class Last_sensor_data : public PollingComponent, public TextSensor {
  public:

    // constructor
    Last_sensor_data() : PollingComponent(500) {}

    void setup() override {
      // This will be called by App.setup()
    }
    void update() override {
      extern std::string my_data;
      extern char have_data_string;
      if (have_data_string == 1)
      {
        publish_state(my_data);
	    have_data_string = 0;
      }

      // This will be called every "update_interval" milliseconds.
      // Publish state

    }
};

2 Likes

Awesome… :raised_hands: will try this out soon.
Would it be OK with you to publish this on my Github? Will of course refer to you, this forum thread and all other contributors you have mentioned here.

Yea sure, i probably should get a GitHub account myself but i never get around to it so go ahead.

Now it is on GitHub: 433MhzFineoffsetRx.
Have not had the time to try it out my self though :upside_down_face:

I have now tried it out and it works great. Added some comments in the code to get it working on Sonoff RF Bridge.
There are some configurations for the decoding of the different sensors that must be made. For example in the “.h” file. I have not documented all these necessary configurations in the Readme…
Feel free to improve it…
Many thanks @swedude :metal:

433MhzFineoffsetRx

1 Like

Very informative project. I have a 433Mhz weather station that I would like to integrate with home assistant. Are there any projects that I can refer to by catching the 433 signals and converting to wifi that it can be displayed in home assistant?

Look for rtl_433 it supports some weather stations or Pilight.

Does anyone know if this project work with Tellstick/Telldus 433Mhz temp/hygro sensor?

Hello, this is very cool and it looks like the only way to get interrupts working in ESP8266 and esphome.

Your library is quite complex for what I need. I created water sensor from mosfet transistor and when it detects water, it triggers LOW interrupt. I can attach it to some pin (GPIO14?). But how I need to modify the .h library just to get esp to wake up by low interrupt on pin 14? The .h library would be much easier right? I did lots of sensors in C++, but I am not that good in creating libraries.

May I please ask you fro advice how to change .h library? I am sure that anone who just wants to wake esp8266 in esphome by interrupt would appreciate your advice, as there is nothing similar elsewehere on internet.

I actually did it first on esp32 with esphome, but defining interrupt pin on deep sleep rasises deep sleep current from 11μA to 1500μA and that is not acceptable for me. So I want to try esp8266 with lower level function.

Note that OenMQTTGateway now supports RTL_433 on ESP32
https://docs.openmqttgateway.com/use/rf.html#rtl-433-device-decoders

And with it the fine offset protocol:

Registering protocol [18] "Fine Offset Electronics, WH2, WH5

You will need to add a CC1101 transceiver to the ESP32.

1 Like

Hello I have never tried sleep on esp8266 but here are a minimal interrupt hope it helps:

interrupt_esp.yaml

esphome:
  name: esp_int
  platform: ESP8266
  board: esp01_1m
  includes:
    - interrupt.h 
wifi:
  networks:
  - ssid: "t"
    password: ""




# Enable Home Assistant API
api:

ota:
logger:


sensor:
- platform: custom
  lambda: |-
    auto my_sensor = new MyCustomSensor();
    App.register_component(my_sensor);
    return {my_sensor->int_counter,my_sensor->int_state};

  sensors:


  - name: "interupt counter"
    accuracy_decimals: 0
    unit_of_measurement: ' '

  - name: "interupt state"
    accuracy_decimals: 0
    unit_of_measurement: ' '


interrupt.h

#include "esphome.h"
#define int_pin 3
boolean interrupt;


void ICACHE_RAM_ATTR  ext_int_1()
{
  interrupt = true;
}


class MyCustomSensor : public PollingComponent {
  public:

    Sensor *int_counter = new Sensor();
    Sensor *int_state = new Sensor();

    unsigned long interupt_counter;
    MyCustomSensor() : PollingComponent(250) { }


    void setup() override {
      Serial.end();//kill serial if you use rx or tx pins...
      //attachInterrupt(int_pin, ext_int_1, CHANGE);
	  //attachInterrupt(int_pin, ext_int_1, RISING);
	  attachInterrupt(int_pin, ext_int_1, FALLING);


    }

    void loop() override
    {
      if (interrupt)
      {
        interupt_counter++;
        int_counter->publish_state(interupt_counter);
        int_state->publish_state(digitalRead(int_pin));
        interrupt = false;
      }

    }


    void update() override 
	{


    }


}
;

good luck and let me know if you make a really low power esp8266 sensor, I might use it too.

Thank yu sir very much for your kindness!

The scripts seem to compile well, but the interrupt does not wake the chip from deep sleep. I commented out Serial.end and I changed trigger to CHANGE, but when I am switching the pin betwen 3V3 and GND esp does not wake up.

in the esphome script, I added

deep_sleep:
  id: deep_sleep_1
  run_duration: 4s
  sleep_duration: 15s # 3806s

and here is end of output log, there deep sleep is set up and triggered, but nothing happens after that.

[13:49:46][C][deep_sleep:020]: Setting up Deep Sleep...
[13:49:46][C][deep_sleep:023]:   Sleep Duration: 15000 ms
[13:49:46][C][deep_sleep:026]:   Run Duration: 4000 ms
[13:49:50][I][deep_sleep:067]: Beginning Deep Sleep
[13:49:50][W][wifi_esp8266:444]: Event: Disconnected ssid='IoT' bssid=EC:AD:E0:D2:FC:DB reason='Association Leave'

I also realized that when sensor is not in deep sleep the interrupt works perfectly. So I think that ESP8266 needs to be configured for interrupt wakeup rught before going to deep sleep. I will investigate.

I found in the documentation that all pins except RESET pin are not active during deep sleep so it is not possible to wake it up by interrupt other than on reset pin…

Has anyone tried this with an ESP32?
I modified the YAML to select ESP32 and a LOLIN32 (also a DEVKIT).
It compiles, but fails in linking with multiple ".iram1[MyCustomSensor::ext_int_1()]+0xnnn): dangerous relocation l32r: literal placed after use" errors.
I have tried a few things, but it definitely does not like interrupts on the ESP32 and I think this is beyond my paygrade :slight_smile:

OK, so I moved the interrupt routines outside of the class: MyCustomSensor{..} block and all is well !!!

1 Like

I have the same problem. Can you share your code? thanks

I was using interrupts to read the SPI pins on a PZEM031 display.

ICACHE inside the class dec did not work so I moved it outside!

The code monitors the pins on the HT1621 SPI comms from the embedded micro to the display controller. The SPI pins are read using interrupts.

See this url:
https://www.webx.dk/oz2cpu/energy-meter/energy-meter.htm

The Files

esph_pzem031.h:

// HT1621 SPI Pin allocation
// Vdd  1 - 9V
// Data 2	-	GPIO_MOSI        23
// WR 	3	-	GPIO_SCLK        18
// CS  	4	-	GPIO_CS          5
// PB   5	-	GPIO
// Vss  6 - GND 

// HT1621 naming convention
// (clockwise from top)
//   A
// F   B
//   G
// E   C
//   D

// bit indexes for LCD segments in LCD controller RAM
//
//   88.88 V    88.88 A
//    voltage    current
//   888.8 kW   8888 kWh
//    power      energy
//

// voltage
//                          A   B   C   D   E   F   G   	// D E F A X C G B
const uint8_t v[4][7] = {{  3,  7,  5,  0,  1,  2,  6}, 	// 0
						  { 11, 15, 13,  8,  9, 10, 14}, 	// 2
						  { 19, 23, 21, 16, 17, 18, 22}, 	// 4
						  { 27, 31, 29, 24, 25, 26, 30}};	// 6
const uint8_t v_d =  12; // decimal 88.88
const uint8_t v_u =  28; // unit (V)
const uint8_t v_l =   4; // label

// current
//                          A   B   C   D   E   F   G   	// D E F A X C G B
const uint8_t c[4][7] = {{ 35, 39, 37, 32, 33, 34, 38}, 	// 8
						  { 43, 47, 45, 40, 41, 42, 46}, 	// A
						  { 51, 55, 53, 48, 49, 50, 54}, 	// B
						  { 59, 63, 61, 56, 57, 58, 62}};	// D
const uint8_t c_d =  44; // decimal 88.88
const uint8_t c_u =  60; // unit (A)
const uint8_t c_l =  36; // label

// power
//                          A   B   C   D   E   F   G   	// A F E D B G C X
const uint8_t p[4][7] = {{ 64, 68, 70, 67, 66, 65, 69}, 	// 10
						  { 72, 76, 78, 75, 74, 73, 77}, 	// 12
						  { 80, 84, 86, 83, 82, 81, 85}, 	// 14
						  { 88, 92, 94, 91, 90, 89, 93}};	// 16
const uint8_t p_d =  87; // decimal 888.8
const uint8_t p_k = 111; // kilo
const uint8_t p_u = 103; // unit (W)
const uint8_t p_l =  79; // label

// energy
//                          A   B   C   D   E   F   G   	// A F E D B G C X
const uint8_t e[4][7] = {{ 96,100,102, 99, 98, 97,101}, 	// 18
						  {104,108,110,107,106,105,109}, 	// 1A
						  {112,116,118,115,114,113,117}, 	// 1B
						  {120,124,126,123,122,121,125}};	// 1D
const uint8_t e_k = 127; // kilo
const uint8_t e_u = 119; // unit (Wh)
const uint8_t e_l =  95; // label

#define RPS 4
#define CPR 5

uint8_t DisplayData[RPS][CPR];	

#define GPIO_CS          5
#define GPIO_SCLK        18
#define GPIO_MISO        19
#define GPIO_MOSI        23
#define ARRAYMAX         128

volatile uint16_t   CSCount;
volatile uint32_t   MOSIData;
volatile uint16_t   MOSIDataIPTR;
uint16_t       		MOSIDataOPTR;
volatile uint32_t   MOSIDataArray[ARRAYMAX];
uint8_t        		mask;
uint8_t        		nibl;
								   
bool 				valid_meter_data;
bool 				start_meter_data;
uint16_t       		allDataSet;
float volts;
float amps;
float watts;
float wattHrs;
// CS rising edge - signals end of current data write
// Transfer data into array and handle pointer, counter
	static void ICACHE_RAM_ATTR GPIO_CS_isr_handler(){
		
		extern volatile uint16_t CSCount;
		extern volatile uint32_t MOSIData;
		extern volatile uint32_t MOSIDataArray[ARRAYMAX];
		extern volatile uint16_t MOSIDataIPTR;
    
		MOSIDataArray[MOSIDataIPTR++] = MOSIData>>1;
		MOSIData = 0;
		MOSIDataIPTR &= ARRAYMAX-1;
		if (CSCount++ > 7) {
			CSCount = 0;
		}
	}

	static void ICACHE_RAM_ATTR GPIO_SCLK_isr_handler(){

// CK rising edge - signal write of current data bit
// Read bit into data 		
		extern volatile uint32_t MOSIData;
		if (digitalRead(GPIO_MOSI)) MOSIData |= 1;
		MOSIData <<= 1;
	}

class BatteryPower : public PollingComponent {
  public:
    Sensor *sensor1 = new Sensor();
    Sensor *sensor2 = new Sensor();
    Sensor *sensor3 = new Sensor();
    Sensor *sensor4 = new Sensor();

 BatteryPower() : PollingComponent(10000) { }

    void setup() override 
	{
extern	volatile uint16_t   CSCount;
extern	volatile uint32_t   MOSIData;
extern	volatile uint16_t   MOSIDataIPTR;
extern	uint16_t       		MOSIDataOPTR;
extern  bool 				start_meter_data;
extern  bool 				valid_meter_data;									  
extern	uint16_t       		allDataSet;
extern	uint8_t 			DisplayData[RPS][CPR];

	  CSCount=0;
	  MOSIData=0;
	  MOSIDataIPTR=0;
	  MOSIDataOPTR=0;
	  start_meter_data=false;
	  valid_meter_data=false;
	  allDataSet = 0;
	  
	  pinMode(GPIO_CS, INPUT_PULLUP);
	  pinMode(GPIO_SCLK, INPUT_PULLUP);
	  pinMode(GPIO_MOSI, INPUT_PULLUP);
	  pinMode(GPIO_MISO, OUTPUT);
	  
	  for (int i=0;i<RPS;i++){
		  for (int j=0;j<CPR;j++){
			  DisplayData[i][j] = 0;
		  }
	  }
	  
//	  Serial.begin(115200);
//	  Serial.println("ESP32 PZEM031 BB Monitor");
	  
	  // Add Interrupts
	  attachInterrupt(digitalPinToInterrupt(GPIO_CS), GPIO_CS_isr_handler, RISING);
	  attachInterrupt(digitalPinToInterrupt(GPIO_SCLK), GPIO_SCLK_isr_handler, RISING);
    }

    void update() override {
      // This is the actual meter reading logic.

extern  bool 				start_meter_data;
extern  bool 				valid_meter_data;
extern	uint16_t       		allDataSet;
extern  float 				volts;
extern  float 				amps;
extern  float 				watts;
extern  float 				wattHrs;
extern	uint8_t 			DisplayData[RPS][CPR];

extern	volatile uint16_t   CSCount;
extern	volatile uint32_t   MOSIData;
extern	volatile uint16_t   MOSIDataIPTR;
extern	uint16_t       		MOSIDataOPTR;
extern	volatile uint32_t   MOSIDataArray[ARRAYMAX];
extern	uint8_t        		mask;
extern	uint8_t        		nibl;

		uint8_t c = 0;
		uint8_t i = 0;
		uint8_t j = 0;
		uint8_t k = 0;
		uint16_t row = 0;
		uint16_t col = 0;
  
        while (MOSIDataIPTR != MOSIDataOPTR) {
		  
		// Valid data
		// Check control
          i = (MOSIDataArray[MOSIDataOPTR] >> 8) & 0x03F;
		  if (i == 0) start_meter_data = true;		// Address 0
		  if (start_meter_data == true) {
//ESP_LOGD("BatteryMonitor", "%4X", MOSIDataArray[MOSIDataOPTR]);

            switch (i) {
                case 0x00: 							// Volts
                  mask = 0xF7; volts=0; break;
                case 0x04:
                  break;
                case 0x08:								// Current
                  mask = 0xF7; amps=0; break;
                case 0x0C:
                  break;
                case 0x10:								// Power
                  mask = 0xFE; watts=0; break;
                case 0x16:
                  break;
                case 0x18:								// Energy
                  mask = 0xFE; wattHrs=0; break;
                case 0x1E:
                  break;
                default: break;
            }
		// Map data
            nibl = (MOSIDataArray[MOSIDataOPTR] & 0xFF);
            if (mask == 0xF7) {
                switch (nibl & mask) {
                    case 0xf5: c='0';break;
                    case 0x05: c='1';break;
                    case 0xd3: c='2';break;
                    case 0x97: c='3';break;
                    case 0x27: c='4';break;
                    case 0xB6: c='5';break;
                    case 0xF6: c='6';break;
                    case 0x15: c='7';break;
                    case 0xF7: c='8';break;
                    case 0xB7: c='9';break;
                    default: c=' ';break;
                }
            }
            else if (mask == 0xFE) {
                switch (nibl & mask) {
                    case 0xfA: c='0';break;
                    case 0x0A: c='1';break;
                    case 0xBC: c='2';break;
                    case 0x9E: c='3';break;
                    case 0x4E: c='4';break;
                    case 0xD6: c='5';break;
                    case 0xF6: c='6';break;
                    case 0x8A: c='7';break;
                    case 0xFE: c='8';break;
                    case 0xDE: c='9';break;
                    default: c=' ';break;
               }
            }

            if (((c >= '0') && (c <= '9')) || (c == ' ')) {
			
					row = i/8;
					col = (i%8)/2;
					DisplayData[row][col] = c;
					
				if (c != ' ') {					
					c = c -'0';
				// Update variable according to LCD address (set above)
					switch (i) {
						
						j |= (1 << (i/2));		// Set bit in byte array to indicate update
						
						case 0x00: volts=c; 			allDataSet |= 0x0001; break;
						case 0x02: volts=volts*10+c; 	allDataSet |= 0x0002; break;
						case 0x04: volts=volts*10+c; 	allDataSet |= 0x0004; break;
						case 0x06: volts=volts*10+c; 	allDataSet |= 0x0008; break;
						
						case 0x08: amps=c; 			allDataSet |= 0x0010; break;
						case 0x0A: amps=amps*10+c; 	allDataSet |= 0x0020; break;
						case 0x0C: amps=amps*10+c; 	allDataSet |= 0x0040; break;
						case 0x0E: amps=amps*10+c; 	allDataSet |= 0x0080; break;
						
						case 0x10: watts=c; 			allDataSet |= 0x0100; break;
						case 0x12: watts=watts*10+c; 	allDataSet |= 0x0200; break;
						case 0x14: watts=watts*10+c; 	allDataSet |= 0x0400; break;
						case 0x16: watts=watts*10+c; 	allDataSet |= 0x0800; break;
						
						case 0x18: wattHrs=c; 				allDataSet |= 0x1000; break;
						case 0x1A: wattHrs=wattHrs*10+c; 	allDataSet |= 0x2000; break;
						case 0x1C: wattHrs=wattHrs*10+c; 	allDataSet |= 0x4000; break;
						case 0x1E: wattHrs=wattHrs*10+c; 	allDataSet |= 0x8000; valid_meter_data = true; break;
						
						default: break;
					}
				}
				else {
					switch (i) {
						j |= (1 << (i/2));
						
						case 0x00: allDataSet |= 0x0001; break;
						case 0x02: allDataSet |= 0x0002;  break;
						case 0x04: allDataSet |= 0x0004;  break;
						case 0x06: allDataSet |= 0x0008;  break;
						
						case 0x08: allDataSet |= 0x0010; break;
						case 0x0A: allDataSet |= 0x0020; break;
						case 0x0C: allDataSet |= 0x0040; break;
						case 0x0E: allDataSet |= 0x0080; break;
						
						case 0x10: allDataSet |= 0x0100; break;
						case 0x12: allDataSet |= 0x0200; break;
						case 0x14: allDataSet |= 0x0400; break;
						case 0x16: allDataSet |= 0x0800; break;
						
						case 0x18: allDataSet |= 0x1000; break;
						case 0x1A: allDataSet |= 0x2000; break;
						case 0x1C: allDataSet |= 0x4000; break;
						case 0x1E: allDataSet |= 0x8000; break;
						
						default: break;
					}						
				}
			}
            MOSIDataOPTR++;
            MOSIDataOPTR &= (ARRAYMAX-1);           
          }
		  
		  if ((valid_meter_data == true) && (allDataSet == 0xFFFF)) {
			  
			  volts /= 100;
			  amps  /= 100;
			  watts /= 10;
			  
			  if ((volts >= 0) && (volts <= 50)) sensor1->publish_state(volts);
			  if ((amps >= 0) && (amps <= 100)) sensor2->publish_state(amps);
			  if ((watts >= 0) && (watts <= 1000)) sensor3->publish_state(watts);
			  if ((wattHrs >= 0) && (wattHrs < 10000)) sensor4->publish_state(wattHrs);
			  start_meter_data = false;
			  valid_meter_data = false;	  
ESP_LOGD("BatteryMonitor", " - update   %4.2f %4.2f %4.1f %4.1f",volts,amps,watts,wattHrs);		
ESP_LOGD("BatteryMonitor", " - update   %04X, %04X, %s, %s, %s, %s", j, allDataSet, &DisplayData[0][0], &DisplayData[1][0], &DisplayData[2][0], &DisplayData[3][0]);
			  allDataSet = 0; j = 0;	
		    }
		}
		
ESP_LOGD("BatteryMonitor", " - stored %4.2f %4.2f %4.1f %4.1f",volts,amps,watts,wattHrs);
	
    }
};

esph_pzem031.yaml

substitutions:
#   # https://esphome.io/guides/configuration-types.html#substitutions
  device_name: pzem031              # hostname & entity_id
  friendly_name: BatteryPower    # Displayed in HA frontend              

esphome:
  name: ${device_name}
  platform: ESP32
  board: esp32doit-devkit-v1
  includes:
    - esph_pzem031.h   

wifi:
  networks:
    # https://esphome.io/components/wifi
    ssid: !secret wifissid
    password: !secret wifipass

  fast_connect: true
  reboot_timeout: 15min
  
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: ${friendly_name}_AP
    password: !secret wifipass
    ap_timeout: 5min

web_server:
  port: 80
  # https://esphome.io/components/web_server.html

logger:
  # https://esphome.io/components/logger

api:
  password: !secret esphome_api_password
  # https://esphome.io/components/api

ota:
  password: !secret esphome_ota_password
  # https://esphome.io/components/ota
  
captive_portal:

sensor:
- platform: custom
  lambda: |-
    auto my_sensor = new BatteryPower();
    App.register_component(my_sensor);
    return {my_sensor->sensor1,my_sensor->sensor2,
            my_sensor->sensor3,my_sensor->sensor4};

  sensors:   
  - name: "BPvolts"
    unit_of_measurement: 'V'
    accuracy_decimals: 2

  - name: "BPamps"
    unit_of_measurement: 'A'
    accuracy_decimals: 2      

  - name: "BPwatts"
    unit_of_measurement: 'W'
    accuracy_decimals: 1

  - name: "BPwattHrs"
    unit_of_measurement: 'Wh'
    accuracy_decimals: 0      

Many thanks. I tried to move the interrupt routine outside of the class. Now I can compile and load into esp32, but esp32 hangs as soon as the interrupt signal arrives :frowning:
The same code works on esp8266 and I can read sensor data

wr2_rx.h

char have_data_string;
std::string my_data;
int have_data_sensor;
float temp;
float hum;

 static void ICACHE_RAM_ATTR ext_int_1()
{	
  static unsigned long edgeTimeStamp[3] = {0, };  // Timestamp of edges
  static bool skip;

  // Filter out too short pulses. This method works as a low pass filter.  (borroved from new remote reciever)
  edgeTimeStamp[1] = edgeTimeStamp[2];
  edgeTimeStamp[2] = micros();

  if (skip) {
    skip = false;
    return;
  }

  if (edgeTimeStamp[2]-edgeTimeStamp[1] < 200) {
    // Last edge was too short.
    // Skip this edge, and the next too.
    skip = true;
    return;
  }

  unsigned int pulse = edgeTimeStamp[1] - edgeTimeStamp[0];
  edgeTimeStamp[0] = edgeTimeStamp[1];
	
//--------------------------------------------------------
    // 1 is indicated by 500uS pulse
    // wh2_accept from 2 = 400us to 3 = 600us
#define IS_HI_PULSE(interval)   (interval >= 250 && interval <= 750)
    // 0 is indicated by ~1500us pulse
    // wh2_accept from 7 = 1400us to 8 = 1600us
#define IS_LOW_PULSE(interval)  (interval >= 1200 && interval <= 1750)
    // worst case packet length
    // 6 bytes x 8 bits =48
#define IDLE_HAS_TIMED_OUT(interval) (interval > 1199)
    // our expected pulse should arrive after 1ms
    // we'll wh2_accept it if it arrives after
    // 4 x 200us = 800us
#define IDLE_PERIOD_DONE(interval) (interval >= 751)
#define GOT_PULSE 0x01
#define LOGIC_HI  0x02

      //static uint16_t pulse;
      static byte wh2_flags;
      static byte wh2_accept_flag;
      static byte wh2_packet_state;
      static byte wh2_packet[5];
      static byte wh2_calculated_crc;
      static boolean wh2_valid;
      static byte sampling_state = 0;
      static byte packet_no, bit_no, history;

      switch (sampling_state) {
        case 0: // waiting

          if (IS_HI_PULSE(pulse)) {
            wh2_flags = GOT_PULSE | LOGIC_HI;
            sampling_state = 1;

          } else if (IS_LOW_PULSE(pulse)) {
            wh2_flags = GOT_PULSE; // logic low

          } else {
            sampling_state = 0;
          }
          break;
        case 1: // observe 1ms of idle time

          if (IDLE_HAS_TIMED_OUT(pulse)) {
            sampling_state = 0;
            wh2_packet_state = 1;

          } else if (IDLE_PERIOD_DONE(pulse)) {
            sampling_state = 0;
          }
          else
          {
            sampling_state = 0;
            wh2_packet_state = 1;
          }
          break;
      }


      if (wh2_flags) {


        // acquire preamble
        if (wh2_packet_state == 1) {
          // shift history right and store new value
          history <<= 1;
          // store a 1 if required (right shift along will store a 0)
          if (wh2_flags & LOGIC_HI) {
            history |= 0x01;
          }

          // check if we have a valid start of frame
          // xxxxx110
          if ((history & B00000111) == B00000110) {
            // need to clear packet, and pulseers
            packet_no = 0;
            // start at 1 becuase only need to acquire 7 bits for first packet byte.
            bit_no = 1;
            wh2_packet[0] = wh2_packet[1] = wh2_packet[2] = wh2_packet[3] = wh2_packet[4] = 0;
            // we've acquired the preamble
            wh2_packet_state = 2;
            history = 0xFF;
          }
          wh2_accept_flag = false;
        }
        // acquire packet
        else if (wh2_packet_state == 2) {
          wh2_packet[packet_no] <<= 1;
          if (wh2_flags & LOGIC_HI) {
            wh2_packet[packet_no] |= 0x01;
          }
          bit_no ++;
          if (bit_no > 7) {
            bit_no = 0;
            packet_no ++;
          }
          if (packet_no > 4) {
            // start the sampling process from scratch
            wh2_packet_state = 1;

            wh2_accept_flag = true;
          }
        }
        else
        {
          wh2_accept_flag = false;
        }


        if (wh2_accept_flag) {

          int Sensor_ID = (wh2_packet[0] << 4) + (wh2_packet[1] >> 4);
          int humidity = wh2_packet[3];
          int temperature = ((wh2_packet[1] & B00000111) << 8) + wh2_packet[2];
          // make negative
          if (wh2_packet[1] & B00001000) {
            temperature = -temperature;
          }
          uint8_t crc = 0;
          uint8_t len = 4;
          uint8_t *addr = wh2_packet;
          // Indicated changes are from reference CRC-8 function in OneWire library
          while (len--) {
            uint8_t inbyte = *addr++;
            for (uint8_t i = 8; i; i--) {
              uint8_t mix = (crc ^ inbyte) & 0x80; // changed from & 0x01
              crc <<= 1; // changed from right shift
              if (mix) crc ^= 0x31;// changed from 0x8C;
              inbyte <<= 1; // changed from right shift
            }
          }
          if (crc == wh2_packet[4])
          {
            wh2_valid = true;
          }
          else
          {
            wh2_valid = false;
          }
          extern  int have_data_sensor;
          if (wh2_valid == true && have_data_sensor==0) //avoid change sensor data during update...
          {            
			extern  float temp;
			extern  float hum;
            temp = temperature / 10.0;            
            hum = humidity;
			have_data_sensor = Sensor_ID;
          }
		  
	extern char have_data_string;	  
if (have_data_string==0) //avoid change sensor data during update.
{
          extern std::string my_data;
          my_data = "| Sensor ID: ";
          my_data += to_string(Sensor_ID);
          my_data += " | humidity: ";
          my_data += to_string(humidity);
          my_data += " | temperature: ";
          my_data += to_string(temperature / 10.0);
          my_data += "C | ";
          my_data += (wh2_valid ? "OK " : "BAD" );
          have_data_string = 1;
}

          wh2_accept_flag = false;
        }
        wh2_flags = 0x00;
      }
      //--------------------------------------------------------


}

class MyCustomSensor : public PollingComponent {
  public:

    Sensor *sensor1 = new Sensor();
    Sensor *sensor2 = new Sensor();
		

	
 MyCustomSensor() : PollingComponent(100) { }

    void setup() override 
	{
	Serial.end();
	//For ESP8266,if you are using rx pin for reciever set to 3 below
	//For Sonoff Bridge with direct HW patch use pin 4
	attachInterrupt(4, ext_int_1, CHANGE);			  	  
    }


    void update() override {
      // This is the actual sensor reading logic.
      extern  int have_data_sensor;
      extern  float temp;
      extern  float hum;


	        if (have_data_sensor==1136)
      {     
        sensor1->publish_state(temp);
        sensor2->publish_state(hum);
		have_data_sensor = 0;
      }


	  	       else if (have_data_sensor)
      {
        have_data_sensor = 0;
      }

    }
};



class Last_sensor_data : public PollingComponent, public TextSensor {
  public:

    // constructor
    Last_sensor_data() : PollingComponent(500) {}

    void setup() override {
      // This will be called by App.setup()
    }
    void update() override {
      extern std::string my_data;
      extern char have_data_string;
      if (have_data_string == 1)
      {
        publish_state(my_data);
	    have_data_string = 0;
      }

      // This will be called every "update_interval" milliseconds.
      // Publish state

    }
};