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:
- 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.
- 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);
}
}