PZEM-004T Stale or unavailable sensor values

The PZEM sensors recently and often (every few days) stop updating values and remain constant or the sensor values report as unavailable. The ESP itself is still online but the PZEM sensor values are either stale (the same constant value) or report as unknown. I have a software reset button in the config and the sensors recover after a reset, however the consumption data and charts are off in the energy dashboard until I software reset the ESP.

This started recently, maybe in the past month and happens on three separate ESPs. Previously they worked for several months, if not a year before having an issue. Typical issues in the past were caused by removing high voltage input from the PZEM (unplugging monitored device from the wall).

Seems like something in ESPHome changed recently and is causing this. Appears there have been several modbus PRs recently.

Is anyone else having the same issue?

In case anyone else is having this issue, I’ve converted the code to use default ESPHome modbus functions to read from the PZEM-004T directly instead of using the pzemac component. So far it appears to work fine and is self healing, to an extent.

This has two advantages:

  1. The ESP does not need to be restarted if the PZEM is disconnected from high voltage. Meaning, if mains power is removed and returned, the ESP is able to obtain PZEM values without needing to be power cycled or restarted.
  2. This also allows for the ability to detect when the PZEM is not communicating properly by implementing a lambda filter which updates a watchdog. I’ve used this watchdog to automatically software reboot the ESP if the PZEM is offline for more than 30 minutes.

However, the main disadvantage is the yaml config is much larger and harder to follow.

substitutions:
  name: matts-esp32
  friendly_name: Matts ESP32
  comment: Power meter for Matts TV equipment, Matts HVAC and Matts Nightstand
  ip_address: 10.10.21.13
  pzem_1: HVAC
  pzem_2: TV_Equip
  pzem_3: Nightstand
  pzem_watchdog_timeout: '30' # PZEM watchdog timeout (in minutes)
  pzem_update: '10' # PZEM update interval in seconds

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  comment: ${comment}

esp32:
  board: wemos_d1_mini32
  # board: mhetesp32devkit # this is what's listed in Quin's sketch https://quinled.info/quinled-an-penta-diy-esphome-example-configuration/
  framework:
    type: esp-idf


time:
  - platform: sntp
    id: sntp_time
    servers: !secret ntp_servers
    timezone: America/Los_Angeles
    on_time:

      # sync time
      - seconds: 0
        minutes: 59
        hours: 23
        then:
          - lambda: |-
              auto time = id(sntp_time).now();
              ESP_LOGD("time_sync", "Time before sync: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, time.minute, time.second);
          - lambda: "id(sntp_time).update();"
          - lambda: |-
              auto time = id(sntp_time).now();
              ESP_LOGD("time_sync", "Time after sync: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, time.minute, time.second);

      # reset energy at midnight
      - seconds: 50
        minutes: 59
        hours: 23
        then:
          # Send reset energy command to PZEM 1
          - uart.write:
              id: ubus
              data: [0x02, 0x42, 0x80, 0xE1]
          - delay: 1s
          # Send reset energy command to PZEM 2
          - uart.write:
              id: ubus
              data: [0x03, 0x42, 0x81, 0x71]
          - delay: 1s
          # Send reset energy command to PZEM 3
          - uart.write:
              id: ubus
              data: [0x04, 0x42, 0x83, 0x41]
          - logger.log:
              format: "PZEMs energy reset"
              level: debug

preferences:
  # the default of 1min is far too short--flash chip is rated
  # for approx 100k writes.
  flash_write_interval: "1h"

ethernet:
  type: LAN8720
  mdc_pin: GPIO23
  mdio_pin: GPIO18
  clk_mode: GPIO17_OUT
  phy_addr: 0
  power_pin: GPIO5
  use_address: ${ip_address}

mdns:
  disabled: true

api:
  encryption:
    key: !secret encryption_key
  reboot_timeout: 0s

ota:
  password: !secret ota_password
  platform: esphome

logger:
  baud_rate: 0  # Disable UART logging
  # level: WARN  # normal level for logger
  level: DEBUG # only use this level when debugging an issue
  # level: VERBOSE
  # level: VERY_VERBOSE # only use this level if debug is not producing enough info to debug an issue
  # logs:
  #   hx711: ERROR

# web_server:

uart:

# PZEM UART Bus
  id: ubus
  rx_pin: 15
  tx_pin: 13
  baud_rate: 9600
  stop_bits: 2 # not sure if this should be 1 or 2


modbus:

# PZEM Modbus
  uart_id: ubus
  id: mbus


globals:

  # PZEM watchdog globals
  - id: pzem_1_failed_minutes
    type: int
    restore_value: no
    initial_value: '0'

  - id: pzem_1_last_success
    type: unsigned long
    restore_value: no
    initial_value: '0'

  - id: pzem_2_failed_minutes
    type: int
    restore_value: no
    initial_value: '0'
    
  - id: pzem_2_last_success
    type: unsigned long
    restore_value: no
    initial_value: '0'

  - id: pzem_3_failed_minutes
    type: int
    restore_value: no
    initial_value: '0'
    
  - id: pzem_3_last_success
    type: unsigned long
    restore_value: no
    initial_value: '0'


modbus_controller:

# Modbus controllers for direct PZEM communication
  - id: pzem_1_controller
    address: 2
    modbus_id: mbus
    update_interval: ${pzem_update}s
    
  - id: pzem_2_controller
    address: 3
    modbus_id: mbus
    update_interval: ${pzem_update}s

  - id: pzem_3_controller
    address: 4
    modbus_id: mbus
    update_interval: ${pzem_update}s


button:

  # Virtual Restart
  - platform: restart
    name: "ESP Restart"
    id: restart_button

# Manual Flash Write
  - platform: template
    name: "Save Preferences to Flash"
    id: save_preferences_button
    icon: "mdi:content-save"
    entity_category: config
    disabled_by_default: true
    on_press:
      - lambda: |-
          ESP_LOGI("preferences", "Manual flash write triggered");
          global_preferences->sync();
          ESP_LOGI("preferences", "Flash write completed");

# Reset Energy for pzem_1
  - platform: template
    id: reset_pzem_1_energy
    name: "${pzem_1} Energy Reset"
    entity_category: config
    disabled_by_default: true
    on_press:
      - uart.write:
          id: ubus
          data: [0x02, 0x42, 0x80, 0xE1]

# Reset Energy for pzem_2
  - platform: template
    id: reset_pzem_2_energy
    name: "${pzem_2} Energy Reset"
    entity_category: config
    disabled_by_default: true
    on_press:
      - uart.write:
          id: ubus
          data: [0x03, 0x42, 0x81, 0x71]

# Reset Energy for pzem_2
  - platform: template
    id: reset_pzem_3_energy
    name: "${pzem_3} Energy Reset"
    entity_category: config
    disabled_by_default: true
    on_press:
      - uart.write:
          id: ubus
          data: [0x04, 0x42, 0x83, 0x41]


sensor:

# Uptime Sensor
  - platform: uptime
    name: "ESP Uptime"
    
# PZEM 1 Modbus Sensors
  # Power sensor - Read from registers 0x0003-0x0004 (32-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_1_controller
    id: pzem_1_power
    name: "${pzem_1} Power"
    address: 0x0003  # Power register
    register_type: read
    value_type: U_DWORD_R  # 32-bit unsigned, reversed byte order
    accuracy_decimals: 3
    unit_of_measurement: "kW"
    device_class: power
    state_class: measurement
    filters:
      - lambda: |-
          // Update timestamp BEFORE any filtering
          id(pzem_1_last_success) = millis();
          if (id(pzem_1_failed_minutes) > 0) {
            ESP_LOGI("pzem_watchdog", "${pzem_1} communication restored");
            id(pzem_1_failed_minutes) = 0;
          }
          id(pzem_1_connected).publish_state(true);
          return x;
      - multiply: 0.0001  # PZEM reports power in 0.1W units, convert to kW
      - or:
        - throttle: 1min
        - delta: .01
    skip_updates: 0  # Never skip updates
    
  # Energy sensor - Read from registers 0x0005-0x0006 (32-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_1_controller
    id: pzem_1_energy
    name: "${pzem_1} Energy"
    address: 0x0005  # Energy register
    register_type: read
    value_type: U_DWORD_R  # 32-bit unsigned, reversed byte order
    accuracy_decimals: 3
    unit_of_measurement: "kWh"
    device_class: energy
    state_class: total_increasing
    filters:
      - multiply: 0.001  # PZEM reports energy in Wh, convert to kWh
      - or:
        - throttle: 1min
        - delta: .01
    skip_updates: 0  # Never skip updates


# PZEM 2 Modbus Sensors
  # Power sensor - Read from registers 0x0003-0x0004 (32-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_2_controller
    id: pzem_2_power
    name: "${pzem_2} Power"
    address: 0x0003  # Power register
    register_type: read
    value_type: U_DWORD_R  # 32-bit unsigned, reversed byte order
    accuracy_decimals: 3
    unit_of_measurement: "kW"
    device_class: power
    state_class: measurement
    filters:
      - lambda: |-
          // Update timestamp BEFORE any filtering
          id(pzem_2_last_success) = millis();
          if (id(pzem_2_failed_minutes) > 0) {
            ESP_LOGI("pzem_watchdog", "${pzem_2} communication restored");
            id(pzem_2_failed_minutes) = 0;
          }
          id(pzem_2_connected).publish_state(true);
          return x;
      - multiply: 0.0001  # PZEM reports power in 0.1W units, convert to kW
      - or:
        - throttle: 1min
        - delta: .01
    skip_updates: 0  # Never skip updates
    
  # Energy sensor - Read from registers 0x0005-0x0006 (32-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_2_controller
    id: pzem_2_energy
    name: "${pzem_2} Energy"
    address: 0x0005  # Energy register
    register_type: read
    value_type: U_DWORD_R  # 32-bit unsigned, reversed byte order
    accuracy_decimals: 3
    unit_of_measurement: "kWh"
    device_class: energy
    state_class: total_increasing
    filters:
      - multiply: 0.001  # PZEM reports energy in Wh, convert to kWh
      - or:
        - throttle: 1min
        - delta: .01
    skip_updates: 0  # Never skip updates


# PZEM 3 Modbus Sensors
  # Power sensor - Read from registers 0x0003-0x0004 (32-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_3_controller
    id: pzem_3_power
    name: "${pzem_3} Power"
    address: 0x0003  # Power register
    register_type: read
    value_type: U_DWORD_R  # 32-bit unsigned, reversed byte order
    accuracy_decimals: 3
    unit_of_measurement: "kW"
    device_class: power
    state_class: measurement
    filters:
      - lambda: |-
          // Update timestamp BEFORE any filtering
          id(pzem_3_last_success) = millis();
          if (id(pzem_3_failed_minutes) > 0) {
            ESP_LOGI("pzem_watchdog", "${pzem_3} communication restored");
            id(pzem_3_failed_minutes) = 0;
          }
          id(pzem_3_connected).publish_state(true);
          return x;
      - multiply: 0.0001  # PZEM reports power in 0.1W units, convert to kW
      - or:
        - throttle: 1min
        - delta: .01
    skip_updates: 0  # Never skip updates
    
  # Energy sensor - Read from registers 0x0005-0x0006 (32-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_3_controller
    id: pzem_3_energy
    name: "${pzem_3} Energy"
    address: 0x0005  # Energy register
    register_type: read
    value_type: U_DWORD_R  # 32-bit unsigned, reversed byte order
    accuracy_decimals: 3
    unit_of_measurement: "kWh"
    device_class: energy
    state_class: total_increasing
    filters:
      - multiply: 0.001  # PZEM reports energy in Wh, convert to kWh
      - or:
        - throttle: 1min
        - delta: .01
    skip_updates: 0  # Never skip updates

# PZEM Watchdog sensors
  - platform: template
    name: "${pzem_1} Failed Minutes"
    id: pzem_1_failed_minutes_sensor
    lambda: |-
      return id(pzem_1_failed_minutes);
    update_interval: 60s
    entity_category: diagnostic
    unit_of_measurement: "min"
    
  - platform: template
    name: "${pzem_2} Failed Minutes"
    id: pzem_2_failed_minutes_sensor
    lambda: |-
      return id(pzem_2_failed_minutes);
    update_interval: 60s
    entity_category: diagnostic
    unit_of_measurement: "min"

  - platform: template
    name: "${pzem_3} Failed Minutes"
    id: pzem_3_failed_minutes_sensor
    lambda: |-
      return id(pzem_3_failed_minutes);
    update_interval: 60s
    entity_category: diagnostic
    unit_of_measurement: "min"


# Binary sensors for PZEM communication status
binary_sensor:
  - platform: template
    name: "${pzem_1} Connected"
    id: pzem_1_connected
    device_class: connectivity
    entity_category: diagnostic
    disabled_by_default: true
    lambda: |-
      // Check if we've had a successful read in the last interval
      // Convert seconds to milliseconds for comparison
      unsigned long disconnect_time_ms = ${pzem_update} * 3 * 1000;  // 30 seconds in milliseconds
      if (id(pzem_1_last_success) == 0) return false;  // Never connected
      unsigned long time_since = millis() - id(pzem_1_last_success);
      return time_since < disconnect_time_ms;
    filters:
      - delayed_on_off: 2s  # Add small delay to prevent flapping
      
  - platform: template
    name: "${pzem_2} Connected"
    id: pzem_2_connected
    device_class: connectivity
    entity_category: diagnostic
    disabled_by_default: true
    lambda: |-
      // Check if we've had a successful read in the last interval
      // Convert seconds to milliseconds for comparison
      unsigned long disconnect_time_ms = ${pzem_update} * 3 * 1000;  // 30 seconds in milliseconds
      if (id(pzem_2_last_success) == 0) return false;  // Never connected
      unsigned long time_since = millis() - id(pzem_2_last_success);
      return time_since < disconnect_time_ms;
    filters:
      - delayed_on_off: 2s  # Add small delay to prevent flapping

  - platform: template
    name: "${pzem_3} Connected"
    id: pzem_3_connected
    device_class: connectivity
    entity_category: diagnostic
    disabled_by_default: true
    lambda: |-
      // Check if we've had a successful read in the last interval
      // Convert seconds to milliseconds for comparison
      unsigned long disconnect_time_ms = ${pzem_update} * 3 * 1000;  // 30 seconds in milliseconds
      if (id(pzem_3_last_success) == 0) return false;  // Never connected
      unsigned long time_since = millis() - id(pzem_3_last_success);
      return time_since < disconnect_time_ms;
    filters:
      - delayed_on_off: 2s  # Add small delay to prevent flapping

# Combined problem sensor
  - platform: template
    name: "PZEM Problem Detected"
    id: pzem_problem
    device_class: problem
    entity_category: diagnostic
    lambda: |-
      // Problem exists if either PZEM is disconnected
      return !id(pzem_1_connected).state || !id(pzem_2_connected).state || !id(pzem_3_connected).state;
    filters:
      - delayed_on: 5s  # Only show problem after 5 seconds of disconnection


# Interval to check PZEM health every minute
interval:
  - interval: 1min
    then:
      - lambda: |-
          unsigned long current_time = millis();
          
          // Check PZEM 1
          if (id(pzem_1_last_success) == 0 || (current_time - id(pzem_1_last_success)) > 60000) {
            id(pzem_1_failed_minutes)++;
            ESP_LOGW("pzem_watchdog", "${pzem_1} no response for %d minutes", id(pzem_1_failed_minutes));
          }
          
          // Check PZEM 2
          if (id(pzem_2_last_success) == 0 || (current_time - id(pzem_2_last_success)) > 60000) {
            id(pzem_2_failed_minutes)++;
            ESP_LOGW("pzem_watchdog", "${pzem_2} no response for %d minutes", id(pzem_2_failed_minutes));
          }

          // Check PZEM 3
          if (id(pzem_3_last_success) == 0 || (current_time - id(pzem_3_last_success)) > 60000) {
            id(pzem_3_failed_minutes)++;
            ESP_LOGW("pzem_watchdog", "${pzem_3} no response for %d minutes", id(pzem_3_failed_minutes));
          }
          
          // Check if either PZEM has exceeded the timeout
          if (id(pzem_1_failed_minutes) >= ${pzem_watchdog_timeout} || id(pzem_2_failed_minutes) >= ${pzem_watchdog_timeout}) {
            ESP_LOGE("pzem_watchdog", "PZEM communication timeout exceeded! Restarting ESP...");
            // Reset counters before restart
            id(pzem_1_failed_minutes) = 0;
            id(pzem_2_failed_minutes) = 0;
            // Trigger restart
            id(restart_button).press();
          }
          
  # Check connection status more frequently
  - interval: 5s
    then:
      - lambda: |-
          unsigned long current_time = millis();
          
          // Update PZEM 1 connected status
          if (id(pzem_1_last_success) > 0) {
            unsigned long time_since = current_time - id(pzem_1_last_success);
            bool should_be_connected = time_since < ${pzem_update} * 3 * 1000;
            if (id(pzem_1_connected).state != should_be_connected) {
              id(pzem_1_connected).publish_state(should_be_connected);
            }
          }
          
          // Update PZEM 2 connected status
          if (id(pzem_2_last_success) > 0) {
            unsigned long time_since = current_time - id(pzem_2_last_success);
            bool should_be_connected = time_since < ${pzem_update} * 3 * 1000;
            if (id(pzem_2_connected).state != should_be_connected) {
              id(pzem_2_connected).publish_state(should_be_connected);
            }
          }

          // Update PZEM 3 connected status
          if (id(pzem_3_last_success) > 0) {
            unsigned long time_since = current_time - id(pzem_3_last_success);
            bool should_be_connected = time_since < ${pzem_update} * 3 * 1000;
            if (id(pzem_3_connected).state != should_be_connected) {
              id(pzem_3_connected).publish_state(should_be_connected);
            }
          }
3 Likes

Same issue here after updating esphome… I will later try your approach and add the other sensors to the yaml file, that way I won’t break my automations

I’ve noticed this issue off and on for almost 2 years. The pzrm will report that it is seeing X watts and keep incrementing the energy usage as well.

Super stoked that you figured this out, I was gearing up to take a look at SRC to see if I could find where the race condition is (I assume it’s a race condition).

Thanks!