Winsen ZPHS01B UART component

Hi there.

I have a Winsen sensor ZPHS01B, and have a problem with reading it. Here is my code, everything compile without an error, but after this I receive the error for every byte which I tried to read as follow:
“[20:44:51][E][uart:015]: Reading from UART timed out at byte 19!”

my YAML:

esphome:
  name: weather-station-zphs01b
  includes:
      - zphs01b.h

esp8266:
  board: esp12e

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "2EWouZDJjwpiTYC2AZYDiLf02qfqcEoYfjbwQbp9gFw="

ota:
  password: "20711d6ba8ee983af24756ee2b343ce3"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Weather-Station-Zphs01B"
    password: "v5bRYOHxwMPm"

captive_portal:

web_server:
  port: 80

uart:
  - id: uart_zphs01b
    rx_pin: GPIO14
    tx_pin: GPIO12
    baud_rate: 9600    


sensor:
  - platform: template
    name: PM1.0
    id: zphs01b_pm1
    unit_of_measurement: ug/m3
    accuracy_decimals: 0
    lambda: "return {};"

  - name: PM2.5
    platform: template
    id: zphs01b_pm25
    unit_of_measurement: ug/m3
    accuracy_decimals: 0
    lambda: "return {};"

  - name: PM10
    platform: template
    id: zphs01b_pm10
    unit_of_measurement: ug/m3
    accuracy_decimals: 0
    lambda: "return {};"

  - name: CO2
    platform: template
    id: zphs01b_co2
    unit_of_measurement: ppm
    accuracy_decimals: 0
    lambda: "return {};"

  - name: TVOC
    platform: template
    id: zphs01b_voc
    unit_of_measurement: grades
    accuracy_decimals: 0
    lambda: "return {};"

  - name: Temperature
    platform: template
    id: zphs01b_temp
    unit_of_measurement: "°C"
    accuracy_decimals: 0
    lambda: "return {};"

  - name: Humidity
    platform: template
    id: zphs01b_hum
    unit_of_measurement: "%"
    accuracy_decimals: 0
    lambda: "return {};"

  - name: CH2O
    platform: template
    id: zphs01b_ch2o
    unit_of_measurement: ppm
    accuracy_decimals: 0
    lambda: "return {};"

  - name: CO
    platform: template
    id: zphs01b_co
    unit_of_measurement: ppm
    accuracy_decimals: 0
    lambda: "return {};"

  - name: O3
    platform: template
    id: zphs01b_o3
    unit_of_measurement: ppm
    accuracy_decimals: 0
    lambda: "return {};"

  - name: NO2
    platform: template
    id: zphs01b_no2
    unit_of_measurement: ppm
    accuracy_decimals: 0
    lambda: "return {};"

custom_component:
  - lambda: |-
      auto zphs01b = new WinsenZPHS01BSensor(id(uart_zphs01b), id(zphs01b_pm1), id(zphs01b_pm25), id(zphs01b_pm10), id(zphs01b_co2), id(zphs01b_voc), id(zphs01b_temp), id(zphs01b_hum), id(zphs01b_ch2o), id(zphs01b_co), id(zphs01b_o3), id(zphs01b_no2));
      App.register_component(zphs01b);
      return {zphs01b};

and my zphs01b.h

#include "esphome.h"

int pm1;
int pm25;
int pm10;
int co2;
int voc;
float temp;
float hum;
float ch2o;
float co;
float o3;
float no2;
int chk;


class WinsenZPHS01BSensor : public PollingComponent, public UARTDevice {
 Sensor *xpm1 {nullptr};
 Sensor *xpm25 {nullptr};
 Sensor *xpm10 {nullptr};
 Sensor *xco2 {nullptr};
 Sensor *xvoc {nullptr};
 Sensor *xtemp {nullptr};
 Sensor *xhum {nullptr};
 Sensor *xch2o {nullptr};
 Sensor *xco {nullptr};
 Sensor *xo3 {nullptr};
 Sensor *xno2 {nullptr};
 
 public:
   WinsenZPHS01BSensor(UARTComponent *parent, Sensor *pm1, Sensor *pm25, Sensor *pm10, Sensor *co2, Sensor *voc, Sensor *temp, Sensor *hum, Sensor *ch2o, Sensor *co, Sensor *o3, Sensor *no2)  : UARTDevice(parent) , xpm1(pm1), xpm25(pm25), xpm10(pm10), xco2(co2), xvoc(voc), xtemp(temp), xhum(hum), xch2o(ch2o), xco(co), xo3(o3), xno2(no2) {}
   
   void setup() override {
   this->set_update_interval(5000);
   }
   
   void loop() override {
   
   }
   
   void update() override {
   const uint8_t cmd[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
   for (int i = 0; i < 9; i++) {
   write_array(cmd, sizeof(cmd));
   }
   byte s[26];
 
   for (int i = 0; i < 26; i++) {
     s[i] = read_array(s, sizeof(s));
     

     pm1 = s[2] * 256 + s[3];
     pm25 = s[4] * 256 + s[5];
     pm10 = s[6] * 256 + s[7];
     co2 = s[8] * 256 + s[9];
     voc = s[10];

     temp = ((s[11] * 256 + s[12]) - 435) * 0.1;
     hum = s[13] * 256 + s[14];
     ch2o = (s[15] * 256 + s[16]) * 0.001;
     co = (s[17] * 256 + s[18]) * 0.1;
     o3 = (s[19] * 256 + s[20]) * 0.01;
     no2 = (s[21] * 256 + s[22]) * 0.01;

     chk = s[25];
     }
    }
};

And here is a datasheet for this sensor as well

https://robu.in/wp-content/uploads/2021/03/ZPHS01B-All-In-One-Air-Quality-Monitoring-Sensor-Module.pdf

Is there some one who can help to resolve this?

Thank you in advance

The two for loops in your update() function aren’t necessary when using write_array() and read_array(). For each update() call, the 9 command bytes are being transmitted 9 times and 26 attempts are made to read 26 bytes.

Did you have a go-by you used for the class constructor? Passing the custom component the sensors is a neat idea but the implementation doesn’t look complete. I would have expected the pointers to the sensors to be stored to members of the class so the sensor publish_value() could be used to send out the new values.

1 Like

Damn… I need to work on this… @mulcmu I’m some kind of reverse engineer - I’m good with hardware, and I’m trying to redoing codes to hardware, so I’m not good with your explanation, but the matter that you explain it … I’ll try to learn it even I’m near 50th years old :laughing::laughing::laughing: Thank you @mulcmu

The custom UART component makes the software side a bit more complex. Here was an alternative approach that I think would be easier to implement. This link has the background information:

uart:
  baud_rate: 9600
  tx_pin: 12
  rx_pin: 14
  debug:
    direction: RX
    dummy_receiver: true
    sequence:
      - lambda: |-
          UARTDebug::log_hex(direction, bytes,':');  //Log hex data
          if (bytes.size() == 26 ) {
            //only update if expected response bytes returned
            id(zphs01b_pm1).publish_state( bytes[2] * 256 + bytes[3] );
            
            //////////add rest of sensors here////////////////

          }

sensor:
  - platform: template
    name: PM1.0
    id: zphs01b_pm1
    unit_of_measurement: ug/m3
    accuracy_decimals: 0
    lambda: "return {};"

  - name: PM2.5
    platform: template
    id: zphs01b_pm25
    unit_of_measurement: ug/m3
    accuracy_decimals: 0
    lambda: "return {};"

  - name: PM10
    platform: template
    id: zphs01b_pm10
    unit_of_measurement: ug/m3
    accuracy_decimals: 0
    lambda: "return {};"

  - name: CO2
    platform: template
    id: zphs01b_co2
    unit_of_measurement: ppm
    accuracy_decimals: 0
    lambda: "return {};"

  - name: TVOC
    platform: template
    id: zphs01b_voc
    unit_of_measurement: grades
    accuracy_decimals: 0
    lambda: "return {};"

  - name: Temperature
    platform: template
    id: zphs01b_temp
    unit_of_measurement: "°C"
    accuracy_decimals: 0
    lambda: "return {};"

  - name: Humidity
    platform: template
    id: zphs01b_hum
    unit_of_measurement: "%"
    accuracy_decimals: 0
    lambda: "return {};"

  - name: CH2O
    platform: template
    id: zphs01b_ch2o
    unit_of_measurement: ppm
    accuracy_decimals: 0
    lambda: "return {};"

  - name: CO
    platform: template
    id: zphs01b_co
    unit_of_measurement: ppm
    accuracy_decimals: 0
    lambda: "return {};"

  - name: O3
    platform: template
    id: zphs01b_o3
    unit_of_measurement: ppm
    accuracy_decimals: 0
    lambda: "return {};"

  - name: NO2
    platform: template
    id: zphs01b_no2
    unit_of_measurement: ppm
    accuracy_decimals: 0
    lambda: "return {};"


#write command to get new data every 5 seconds
interval:
  - interval: 5s
    then:
      - uart.write:  [0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]     
3 Likes

Let me try it

@mulcmu You are Genius - thank you very much - everything running perfect :heart_eyes::heart_eyes::heart_eyes:

Hi Vladimir, I am interested in using the same component with home assistant… Have you documented what you did/the project in any article, or you follow/find it in already existing one, and may you kindle share it in order to see how to make it?
Thanks in advance

Hi Mike,

There is nothing to document. I use ESPHome with this code

esphome:
  name: weather-station-zphs01b
  #includes:
  #    - zphs01b.h

esp8266:
  board: esp12e

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "2EWouZDJjwpiTYC2AZYDiLf02qfqcEoYfjbwQbp9gFw="

ota:
  password: "20711d6ba8ee983af24756ee2b343ce3"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Weather-Station-Zphs01B"
    password: "v5bRYOHxwMPm"

captive_portal:

web_server:
  port: 80

uart:
  - id: uart_zphs01b
    rx_pin: GPIO14
    tx_pin: GPIO12
    baud_rate: 9600
    debug:
      direction: RX
      dummy_receiver: true
      sequence:
        - lambda: |-
            UARTDebug::log_hex(direction, bytes,':');  //Log hex data
            if (bytes.size() == 26 ) {
              //only update if expected response bytes returned
              id(zphs01b_pm1).publish_state( bytes[2] * 256 + bytes[3] );
              id(zphs01b_pm25).publish_state( bytes[4] * 256 + bytes[5] );
              id(zphs01b_pm10).publish_state( bytes[6] * 256 + bytes[7] );
              id(zphs01b_co2).publish_state( bytes[8] * 256 + bytes[9] );
              id(zphs01b_voc).publish_state( bytes[10] );
              id(zphs01b_temp).publish_state( ( ( bytes[11] * 256 + bytes[12] ) - 435 ) * 0.1 );
              id(zphs01b_hum).publish_state( bytes[13] * 256 + bytes[14] );
              id(zphs01b_ch2o).publish_state( ( bytes[15] * 256 + bytes[16] ) * 0.001 );
              id(zphs01b_co).publish_state( ( bytes[17] * 256 + bytes[18] ) * 0.1 );
              id(zphs01b_o3).publish_state( ( bytes[19] * 256 + bytes[20] ) * 0.01 );
              id(zphs01b_no2).publish_state( ( bytes[21] * 256 + bytes[22] ) * 0.01 );
              //////////add rest of sensors here////////////////

            }    


sensor:
  - platform: template
    name: PM1
    id: zphs01b_pm1
    unit_of_measurement: ug/m3
    accuracy_decimals: 2
    lambda: "return {};"

  - name: PM2.5
    platform: template
    id: zphs01b_pm25
    unit_of_measurement: ug/m3
    accuracy_decimals: 2
    lambda: "return {};"

  - name: PM10
    platform: template
    id: zphs01b_pm10
    unit_of_measurement: ug/m3
    accuracy_decimals: 2
    lambda: "return {};"

  - name: CO2
    platform: template
    id: zphs01b_co2
    unit_of_measurement: ppm
    accuracy_decimals: 2
    lambda: "return {};"

  - name: TVOC
    platform: template
    id: zphs01b_voc
    unit_of_measurement: ppb
    accuracy_decimals: 2
    lambda: "return {};"

  - name: Temperature
    platform: template
    id: zphs01b_temp
    unit_of_measurement: "°C"
    accuracy_decimals: 2
    lambda: "return {};"

  - name: Humidity
    platform: template
    id: zphs01b_hum
    unit_of_measurement: "%"
    accuracy_decimals: 2
    lambda: "return {};"

  - name: CH2O
    platform: template
    id: zphs01b_ch2o
    unit_of_measurement: ppm
    accuracy_decimals: 2
    lambda: "return {};"

  - name: CO
    platform: template
    id: zphs01b_co
    unit_of_measurement: ppm
    accuracy_decimals: 2
    lambda: "return {};"

  - name: O3
    platform: template
    id: zphs01b_o3
    unit_of_measurement: ppm
    accuracy_decimals: 2
    lambda: "return {};"

  - name: NO2
    platform: template
    id: zphs01b_no2
    unit_of_measurement: ppm
    accuracy_decimals: 2
    lambda: "return {};"

#write command to get new data every 5 seconds
interval:
  - interval: 5min
    then:
      - uart.write:  [0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]     

Thanks a lot Vladimir. I confirm it is working in my side :+1: