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!

FYI, I opened an issue and pointed it towards this post: Pzemac - stale data · Issue #7197 · esphome/issues · GitHub

2 Likes

Well, I did my version here based on the one you shared @mwolter, it seems to be working so far. I will monitor it meanwhile to see how it behaves.

If anyone wants to use as a baseline (improvements can be done for sure) my cfg/yaml file for two phases/pzems with addresses 1 and 2 is:

substitutions:
  ### System variables ###
  board_type: esp32-s3-devkitc-1
  hostname: "hass-power-meter" # Device hostname .local
  prefix: "Power Meter -"
  wifi_network: !secret wifi_ssid
  wifi_pswd: !secret wifi_password
  wifi_pswd_fallback: !secret wifi_password
  endpoint:  "<your_hostname.local.changeme>"
  wifi_hidden: 'False'
  wifi_fast_connect: 'False'
  ota_pswd: !secret ota_password # OTA update password
  pzem1_addr: "1" # PZEM phase 1 address 
  pzem2_addr: "2" # PZEM phase 2 address 
  phase_no: "2" # Amount of phases to be monitored
  update_s: "2" # Time between measurements

esphome:
  name: $hostname
  comment: PZEM-004T Energy Meter - Up to Two Phases
  friendly_name: ${hostname}
  platformio_options:
    build_flags: -D HAVE_HWSERIAL0
####################################################################################
############################# Address Changing Routine #############################
  # on_boot:
  #   ## configure controller settings at setup
  #   ## make sure priority is lower than setup priority of modbus_controller
  #   priority: -100
  #   then:
  #     - lambda: |-
  #         auto new_address = 0x02;

  #         if(new_address < 0x01 || new_address > 0xF7) // sanity check
  #         {
  #           ESP_LOGE("ModbusLambda", "Address needs to be between 0x01 and 0xF7");
  #           return;
  #         }

  #         esphome::modbus_controller::ModbusController *controller = id(pzem);
  #         auto set_addr_cmd = esphome::modbus_controller::ModbusCommandItem::create_write_single_command(
  #           controller, 0x0002, new_address);

  #         delay(200) ;
  #         controller->queue_command(set_addr_cmd);
  #         ESP_LOGI("ModbusLambda", "PZEM Addr set");
###################################################################################

esp32:
  board: ${board_type}

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "<change.me>"

# Enable OTA updates
ota:
  - platform: esphome
    password: ${ota_pswd}

wifi:
  networks:
    ssid: ${wifi_network}
    password: ${wifi_pswd}
    hidden: ${wifi_hidden}
  fast_connect: ${wifi_fast_connect}
  use_address: ${endpoint}

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "power-meter-fallback"
    password: ${wifi_pswd_fallback}

# Enable webpage if connection to configured wifi is lost
captive_portal:

uart:
  id: ubus
  rx_pin: GPIO02
  tx_pin: GPIO01
  baud_rate: 9600
  stop_bits: 1

modbus:
  uart_id: ubus
  id: mbus
####################################################################################
############################# Address Changing Routine #############################
# modbus:
#   send_wait_time: 200ms
#   id: mod_bus_pzem

# modbus_controller:
#   - id: pzem
#     # The current device address.
#     address: 0xF8
#     # The special address 0xF8 is a broadcast address accepted by any pzem device,
#     # so if you use this address, make sure there is only one pzem device connected
#     # to the uart bus.
#     # address: 0xF8
#     modbus_id: mod_bus_pzem
#     command_throttle: 0ms
#     setup_priority: -10
#     update_interval: 30s
###################################################################################
    
text_sensor:
  # Wifi network info
  - platform: wifi_info
    ip_address:
      id: IP
      name: ${prefix} IP Address
      icon: mdi:ip-network
    ssid:
      id: SSID
      name: ${prefix} WiFi SSID
      icon: mdi:wifi

  # Compilation info
  - platform: version
    id: versao
    name: ${prefix} Version
    icon: mdi:information
    
binary_sensor:
  # Status (connected or disconnected)
  - platform: status
    id: status_esp
    name: ${prefix} Status
    device_class: connectivity

  - platform: template
    name: "{prefix} Phase 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 = ${update_s} * 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: "{prefix} Phase 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 = ${update_s} * 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

# 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;
    filters:
      - delayed_on: 5s  # Only show problem after 5 seconds of disconnection

button:
    # Button to reset the ESP remotelly
  - platform: restart
    id: restart_button
    name: ${prefix} Software Reset
    icon: mdi:restart

  # Reset Energy for pzem_1
  - platform: template
    id: reset_pzem_1_energy
    name: "${prefix} Phase 1 - Energy Reset"
    entity_category: config
    disabled_by_default: true
    on_press:
      - uart.write:
          id: ubus
          # base_addr, 0x42, CRC HSB, CRC LSB
          data: [0x01, 0x42, 0x80, 0x11] # use https://www.lammertbies.nl/comm/info/crc-calculation CRC-16 (Modbus)

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

### Device Specific Config ###
# Enable time component to reset energy at midnight
time:
  - platform: homeassistant
    id: homeassistant_time

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'

# Modbus controllers for direct PZEM communication
modbus_controller:
  - id: pzem_1_controller
    address: ${pzem1_addr}
    modbus_id: mbus
    update_interval: ${update_s}s
    
  - id: pzem_2_controller
    address: ${pzem2_addr}
    modbus_id: mbus
    update_interval: ${update_s}s

sensor:
  # Wifi signal strength
  - platform: wifi_signal
    id: wifi_sinal
    name: ${prefix} Wifi Strength
    icon: mdi:signal
    update_interval: 10s

  # Uptime Sensor
  - platform: uptime
    name: "ESP Uptime"

  ############################# PZEM #1 #############################
  # Voltage value - Read from register 0x0000 (16-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_1_controller
    id: pzem_1_voltage
    icon: mdi:sine-wave
    name: "${prefix} Phase 1 - Voltage"
    address: 0x0000  # Voltage register
    register_type: read
    value_type: U_WORD  # 16-bit unsigned
    accuracy_decimals: 1
    unit_of_measurement: "V"
    device_class: voltage
    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", "Phase 1 communication restored");
            id(pzem_1_failed_minutes) = 0;
          }
          id(pzem_1_connected).publish_state(true);
          return x;
      - multiply: 0.1  # PZEM reports power in 0.1V units, convert to V
    skip_updates: 0  # Never skip updates

  # Current sensor - Read from registers 0x0001-0x0002 (32-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_1_controller
    id: pzem_1_current
    icon: mdi:current-ac
    name: "${prefix} Phase 1 - Current"
    address: 0x0001  # Current register
    register_type: read
    value_type: U_DWORD_R  # 32-bit unsigned, reversed byte order
    accuracy_decimals: 2
    unit_of_measurement: "A"
    device_class: current
    state_class: measurement
    filters:
      - multiply: 0.001  # PZEM reports power in 0.001A units, convert to A
    skip_updates: 0  # Never skip updates
    
  # Power sensor - Read from registers 0x0003-0x0004 (32-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_1_controller
    id: pzem_1_power
    icon: mdi:flash
    name: "${prefix} Phase 1 - Power"
    address: 0x0003  # Power register
    register_type: read
    value_type: U_DWORD_R  # 32-bit unsigned, reversed byte order
    accuracy_decimals: 1
    unit_of_measurement: "W"
    device_class: power
    state_class: measurement
    filters:
      - multiply: 0.1  # PZEM reports power in 0.1W units, convert to W
    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
    icon: mdi:counter
    name: "${prefix} Phase 1 - Total Energy"
    address: 0x0005  # Energy register
    register_type: read
    value_type: U_DWORD_R  # 32-bit unsigned, reversed byte order
    accuracy_decimals: 1
    unit_of_measurement: "kWh"
    device_class: energy
    state_class: total_increasing
    filters:
      - multiply: 0.001  # PZEM reports energy in Wh, convert to kWh
    skip_updates: 0  # Never skip updates

  # Frequency value - Read from register 0x0007 (16-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_1_controller
    id: pzem_1_frequency
    icon: mdi:sine-wave
    name: "${prefix} Phase 1 - Frequency"
    address: 0x0007  # Frequency register
    register_type: read
    value_type: U_WORD  # 16-bit unsigned
    accuracy_decimals: 0
    unit_of_measurement: "Hz"
    device_class: frequency 
    state_class: measurement
    filters:
      - multiply: 0.1  # PZEM reports power in 0.1Hz units, convert to Hz
    skip_updates: 0  # Never skip updates

  # Power Factor value - Read from register 0x0008 (16-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_1_controller
    id: pzem_1_power_facter
    icon: mdi:angle-acute
    name: "${prefix} Phase 1 - Power Factor"
    address: 0x0008  # Frequency register
    register_type: read
    value_type: U_WORD  # 16-bit unsigned
    accuracy_decimals: 0
    unit_of_measurement: "%"
    device_class: power_factor 
    state_class: measurement
    skip_updates: 0  # Never skip updates

  - platform: template
    id: pzem_1_apparent_power
    name: "${prefix} Phase 1 - Apparent Power"
    icon: mdi:alpha-s
    accuracy_decimals: 1
    lambda: |-
        return id(pzem_1_current).state * id(pzem_1_voltage).state;
    unit_of_measurement: "VA"
    device_class: apparent_power 
    state_class: measurement

############################# PZEM #2 #############################
  # Voltage value - Read from register 0x0000 (16-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_2_controller
    id: pzem_2_voltage
    icon: mdi:sine-wave
    name: "${prefix} Phase 2 - Voltage"
    address: 0x0000  # Voltage register
    register_type: read
    value_type: U_WORD  # 16-bit unsigned
    accuracy_decimals: 1
    unit_of_measurement: "V"
    device_class: voltage
    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", "Phase 2 communication restored");
            id(pzem_2_failed_minutes) = 0;
          }
          id(pzem_2_connected).publish_state(true);
          return x;
      - multiply: 0.1  # PZEM reports power in 0.1V units, convert to V
    skip_updates: 0  # Never skip updates

  # Current sensor - Read from registers 0x0001-0x0002 (32-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_2_controller
    id: pzem_2_current
    icon: mdi:current-ac
    name: "${prefix} Phase 2 - Current"
    address: 0x0001  # Current register
    register_type: read
    value_type: U_DWORD_R  # 32-bit unsigned, reversed byte order
    accuracy_decimals: 2
    unit_of_measurement: "A"
    device_class: current
    state_class: measurement
    filters:
      - multiply: 0.001  # PZEM reports power in 0.001A units, convert to A
    skip_updates: 0  # Never skip updates
    
  # Power sensor - Read from registers 0x0003-0x0004 (32-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_2_controller
    id: pzem_2_power
    icon: mdi:flash
    name: "${prefix} Phase 2 - Power"
    address: 0x0003  # Power register
    register_type: read
    value_type: U_DWORD_R  # 32-bit unsigned, reversed byte order
    accuracy_decimals: 1
    unit_of_measurement: "W"
    device_class: power
    state_class: measurement
    filters:
      - multiply: 0.1  # PZEM reports power in 0.1W units, convert to W
    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
    icon: mdi:counter
    name: "${prefix} Phase 2 - Total Energy"
    address: 0x0005  # Energy register
    register_type: read
    value_type: U_DWORD_R  # 32-bit unsigned, reversed byte order
    accuracy_decimals: 1
    unit_of_measurement: "kWh"
    device_class: energy
    state_class: total_increasing
    filters:
      - multiply: 0.001  # PZEM reports energy in Wh, convert to kWh
    skip_updates: 0  # Never skip updates

  # Frequency value - Read from register 0x0007 (16-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_2_controller
    id: pzem_2_frequency
    icon: mdi:sine-wave
    name: "${prefix} Phase 2 - Frequency"
    address: 0x0007  # Frequency register
    register_type: read
    value_type: U_WORD  # 16-bit unsigned
    accuracy_decimals: 0
    unit_of_measurement: "Hz"
    device_class: frequency 
    state_class: measurement
    filters:
      - multiply: 0.1  # PZEM reports power in 0.1Hz units, convert to Hz
    skip_updates: 0  # Never skip updates

  # Power Factor value - Read from register 0x0008 (16-bit)
  - platform: modbus_controller
    modbus_controller_id: pzem_2_controller
    id: pzem_2_power_facter
    icon: mdi:angle-acute
    name: "${prefix} Phase 2 - Power Factor"
    address: 0x0008  # Frequency register
    register_type: read
    value_type: U_WORD  # 16-bit unsigned
    accuracy_decimals: 0
    unit_of_measurement: "%"
    device_class: power_factor 
    state_class: measurement
    skip_updates: 0  # Never skip updates

  - platform: template
    id: pzem_2_apparent_power
    name: "${prefix} Phase 2 - Apparent Power"
    icon: mdi:alpha-s
    accuracy_decimals: 1
    lambda: |-
        return id(pzem_2_current).state * id(pzem_2_voltage).state;
    unit_of_measurement: "VA"
    device_class: apparent_power 
    state_class: measurement

  # ALL phases total aggregation
  - platform: template
    id: pzem_total_active_power
    name: "${prefix} Total Active Power"
    icon: mdi:flash
    accuracy_decimals: 1
    lambda: |-
        if (${phase_no}==1) {
          return id(pzem_1_power).state;
        }
        if (${phase_no}==2) {
          return id(pzem_1_power).state + id(pzem_2_power).state;
        }
    unit_of_measurement: "W"
    update_interval: ${update_s}s
    state_class: measurement
    device_class: power

  - platform: template
    id: pzem_total_apparent_power
    name: "${prefix} Total Apparent Power"
    icon: mdi:alpha-s
    accuracy_decimals: 1
    lambda: |-
        if (${phase_no}==1) {
          return id(pzem_1_apparent_power).state;
        }
        if (${phase_no}==2) {
          return id(pzem_1_apparent_power).state + id(pzem_2_apparent_power).state;
        }
    unit_of_measurement: "VA"
    update_interval: ${update_s}s
    state_class: measurement
    device_class: apparent_power

  - platform: template
    id: pzem_total_power_factor
    name: "${prefix} Total Power Factor"
    icon: mdi:angle-acute
    accuracy_decimals: 0
    lambda: |-
        return id(pzem_total_active_power).state / id(pzem_total_apparent_power).state;
    unit_of_measurement: "%"
    filters:
      - multiply: 100
    update_interval: ${update_s}s
    state_class: measurement
    device_class: power_factor

  - platform: template
    id: ET
    name: "${prefix} Total Energy"
    icon: 'mdi:counter'
    accuracy_decimals: 1
    lambda: |-
        if (${phase_no}==1) {
          return (id(pzem_1_energy).state);
        }
        if (${phase_no}==2) {
            return (id(pzem_1_energy).state + id(pzem_2_energy).state);
        }
    unit_of_measurement: "kWh"
    update_interval: ${update_s}s
    state_class: total_increasing
    device_class: energy

I noticed this issue as well.
I’m going to try the mwolter solution asap.

I can confim that it works, running here without interruption so far

Doesn’t sound like the esphome maintainer is too keen on fixing it (like usual, IME). They asked me to go to the discord to debug and then ignored me, not sure what the deal is there.

Going to implement these fixes in my code since they are working for everyone else. Just glad there is some sort of fix for this. Thanks, @mwolter

1 Like