Monitoring electricity usage is crucial for energy efficiency and cost savings. This tutorial guides you on how to build an IoT-based energy meter using ESP32 and PZEM-004T V3 energy monitoring module to create a smart electricity meter. Unlike basic energy meters, this project offers dual monitoring capabilities: real-time data display on a TFT screen and a web dashboard for remote monitoring.
You can measure voltage, current, power, energy consumption, frequency, and power factor while accessing the data through a local display and a web-based interface. KWh to Watts Calculator
More Energy Monitoring Projects
Required Components
- ESP32 Development Board
- PZEM-004T V3 Energy Meter Module
- CT sensor
- TFT Display
- Jumper Wires
- Breadboard
Understanding the PZEM-004T Module
Accurate power measurement is essential when working on electrical projects. The PZEM-004T is an integrated module for measuring electrical parameters in single-phase AC systems. It accurately measures voltage (80–260V), current (up to 100A), power, energy consumption (kWh), frequency, power factor, and other electrical parameters.
The sensor uses non-invasive current transformers (CTs) to measure the current flowing through a wire without cutting or interrupting the circuit.
This module communicates via Modbus-RTU (RS485), making it an excellent choice for DIY projects, energy monitoring, and IoT-based applications.
Specifications & Features:
- Voltage Measurement: 80-260V with 0.1V resolution (0.5% accuracy)
- Current Measurement:
- 10A model with built-in shunt resistor
- 100A model with an external current transformer
- 0.001A resolution (0.5% accuracy)
- Power Measurement: Up to 2.3kW (10A) or 23kW (100A) with 0.1W resolution
- Energy Monitoring: 0-9999.99kWh with 1Wh resolution (0.5% accuracy)
- Additional Parameters: Frequency (45-65Hz) and Power Factor (0.00-1.00)
- Communication: TTL/RS485 serial interface using Modbus-RTU protocol
- Safety: Built-in galvanic isolation
Current Transformer Options
Current Coil (CT Sensor): A current coil is a current transformer that measures alternating current (AC) flowing through a conductor. The CT sensor used with the PZEM-004T is generally non-invasive, which means it can be clamped around the wire without disconnecting it, making installation easy and safe.
The module comes with three current measurement options:
- 10A Built-in Shunt: Ideal for direct measurement of lower current applications
- 100A Closed CT: Fixed, Closed Current Transformer for permanent installations
- 100A Open/Split CT: Clamp-style transformer for easy installation without disconnecting wires
Pinout & Connections.
Power & Measurement Connections.
- L (Live): Connect to AC mains
- N (Neutral): Connect to AC neutral
- CT+ & CT-: Connect to the external current transformer
- 5V & GND: Power the module
- TX & RX: Serial communication
Note: The TTL interface requires external 5V power supply, and all four pins must be connected for proper communication.
Applications and Use Cases
- Home energy monitoring systems
- Industrial power management
- Renewable energy systems (solar, wind, etc.)
- Laboratory instruments for electrical measurements
- IoT devices for innovative energy solutions
How to Connect PZEM-004T to ESP32 for Power Monitoring
Connecting the PZEM-004T to ESP32 is straightforward and requires just a few connections:
- Connect PZEM-004T 5V to ESP32 Vin
- Connect PZEM-004T GND to ESP32 GND
- Connect PZEM-004T RX to TX2 (ESP32, Pin 17)
- Connect PZEM-004T TX to RX2 (ESP32, Pin 16)
AC Wiring:
- Connect the Live (L) and Neutral (N) of the AC supply (120V/220V) to the corresponding terminals on the PZEM module.
- Current Transformer (CT):
- Open CT (split-core): Clamp around the live wire only (not neutral).
- Closed CT (solid core): Pass the live wire through the core before reconnecting.
ESP32 Code Tutorial: Energy Monitor
Install Required Libraries
- Install the PZEM-004T v3.0 Arduino Library for ModBUS-based energy monitoring.
- Install the TFT display library for your specific display model
- The ESP32 reads voltage, current, power, and energy from the PZEM-004T via Serial2 and prints data every 2 seconds.
Complete ESP32 Power Monitor Source Code
Upload the basic code to ESP32 to verify communication with the PZEM-004T module:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
#include <PZEM004Tv30.h> // Define pins and settings #define PZEM_RX_PIN 16 // ESP32 RX (Connect to PZEM TX) #define PZEM_TX_PIN 17 // ESP32 TX (Connect to PZEM RX) #define MAX_RETRY_ATTEMPTS 2 // Global variables unsigned long measurementInterval = 2000; // Default interval (2 seconds) as in original code unsigned long lastReadTime = 0; PZEM004Tv30 pzem(Serial2, PZEM_RX_PIN, PZEM_TX_PIN); // Structure for PZEM readings struct PzemData { float voltage; float current; float power; float energy; float frequency; float pf; bool isValid; }; void setup() { Serial.begin(115200); Serial2.begin(9600, SERIAL_8N1, PZEM_RX_PIN, PZEM_TX_PIN); delay(1000); Serial.println("\nPZEM-004T V3.0 Power Monitor"); Serial.println("Commands: r (reset), i (toggle interval)"); // Display the PZEM module address Serial.print("PZEM Address: 0x"); Serial.println(pzem.readAddress(), HEX); } void loop() { // Check for commands if (Serial.available()) { handleCommands(Serial.read()); } // Read at specified intervals if (millis() - lastReadTime >= measurementInterval) { lastReadTime = millis(); // Use the original code's approach for reading values individually Serial.print("Address: 0x"); Serial.println(pzem.readAddress(), HEX); float voltage = pzem.voltage(); float current = pzem.current(); float power = pzem.power(); float energy = pzem.energy(); float frequency = pzem.frequency(); float pf = pzem.pf(); // Original error handling approach if(isnan(voltage)){ Serial.println("Error reading voltage"); } else if (isnan(current)) { Serial.println("Error reading current"); } else if (isnan(power)) { Serial.println("Error reading power"); } else if (isnan(energy)) { Serial.println("Error reading energy"); } else if (isnan(frequency)) { Serial.println("Error reading frequency"); } else if (isnan(pf)) { Serial.println("Error reading power factor"); } else { // Print values using improved formatting Serial.println("\n----- PZEM READINGS -----"); Serial.print("Voltage: "); Serial.print(voltage); Serial.println(" V"); Serial.print("Current: "); Serial.print(current); Serial.println(" A"); Serial.print("Power: "); Serial.print(power); Serial.println(" W"); Serial.print("Energy: "); Serial.print(energy, 3); Serial.println(" kWh"); Serial.print("Frequency: "); Serial.print(frequency); Serial.println(" Hz"); Serial.print("Power Factor: "); Serial.println(pf); Serial.print("Apparent Power: "); Serial.print(voltage * current, 1); Serial.println(" VA"); Serial.println("-------------------------"); } Serial.println(); } } void handleCommands(char cmd) { switch (cmd) { case 'r': // Reset energy counter Serial.println("Resetting energy counter..."); if (pzem.resetEnergy()) { Serial.println("Reset successful"); } else { Serial.println("Reset failed"); } break; case 'i': // Toggle measurement interval if (measurementInterval == 2000) { Serial.println("Interval: 5s"); measurementInterval = 5000; } else if (measurementInterval == 5000) { Serial.println("Interval: 10s"); measurementInterval = 10000; } else { Serial.println("Interval: 2s"); measurementInterval = 2000; } break; } } |
Upload the code to ESP32, power on the device, and PZEM-004T.
Upload the code to ESP32, power on the device, and PZEM-004T. Then, open the Serial Monitor at 115200 baud to view AC voltage, current, power, energy, frequency, and power factor readings.
IoT Energy Meter with ESP32 & PZEM-004T V3 (TFT + Web Dashboard)
The system successfully reads and displays AC energy parameters using the PZEM-004T sensor and ESP32. The data is locally accessible (TFT display) and remote (web interface), making it a perfect IoT-based AC Energy Meter.
Wiring & Connections
TFT Display to ESP32 Connections
TFT Pin | ESP32 Pin | Description |
---|---|---|
VCC | 3.3V | Power supply (3.3V) |
GND | GND | Ground |
CS | GPIO5 | Chip Select (SPI) |
RST | GPIO22 | Reset (optional if tied high) |
DC (RS) | GPIO21 | Data/Command Control |
SDA (MOSI) | GPIO23 | SPI Data line (VSPI MOSI) |
SCK | GPIO18 | SPI Clock (VSPI SCLK) |
LED (BL) | 3.3V | Backlight (use resistor) |
PZEM-004T to ESP32 Connections
PZEM-004T Pin | ESP32 Pin | Description |
---|---|---|
5V | Vin (5V) | Power supply for PZEM module |
GND | GND | Ground |
TX | GPIO16 (RX2) | Serial communication (data from PZEM to ESP32) |
RX | GPIO17 (TX2) | Serial communication (data from ESP32 to PZEM) |
PZEM-004T AC Wiring
PZEM-004T Terminal | Connection | Description |
---|---|---|
L (Live) | AC mains | Connect to AC power supply live wire |
N (Neutral) | AC neutral | Connect to AC power supply neutral wire |
CT+ & CT- | Current Transformer | Connect to the external current transformer |
Note: Always confirm that the power is disconnected when connecting, especially for the AC wiring. For safety.
Complete System Features:
- Real-time Monitoring: Measures voltage, current, power, energy, frequency, and power factor
- 1.8-inch TFT Display: Shows live energy data with color-coded alerts
- Web Dashboard: Responsive interface with AJAX-based dynamic updates
- Dual Access: Monitor data locally on TFT or remotely via web
- Constant Energy Tracking: Energy (kWh) is stored until manually reset
Creating a Responsive Web Dashboard for ESP32 Energy Monitor
The code can be modified to add HTML, CSS, and JavaScript for an improved web server experience. This enables real-time IoT-based AC energy monitoring with a beautiful, responsive, and AJAX-powered dashboard.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 |
#include <PZEM004Tv30.h> #include <SPI.h> #include <Adafruit_GFX.h> #include <Adafruit_ST7735.h> #include <WiFi.h> #include <WebServer.h> #include <ArduinoJson.h> // Define pins for PZEM #define PZEM_RX_PIN 16 #define PZEM_TX_PIN 17 // TFT display pins #define TFT_CS 5 #define TFT_DC 21 #define TFT_RST 22 #define TFT_MOSI 23 #define TFT_SCLK 18 // WiFi credentials const char* ssid = "ESP"; // Replace with your WiFi SSID const char* password = "aaaaaaaa"; // Replace with your WiFi password // Web server port WebServer server(80); // Colors #define BLACK 0x0000 #define BLUE 0x001F #define RED 0xF800 #define GREEN 0x07E0 #define YELLOW 0xFFE0 #define WHITE 0xFFFF #define DARKGRAY 0x4208 #define CYAN 0x07FF // Global variables unsigned long measurementInterval = 2000; // Interval between PZEM readings (milliseconds) unsigned long lastReadTime = 0; unsigned long screenRefreshTime = 0; const unsigned long SCREEN_REFRESH_INTERVAL = 1000; // Interval for screen refresh (milliseconds) bool layoutDrawn = false; unsigned long notificationEndTime = 0; String currentNotification = ""; uint16_t notificationColor = WHITE; unsigned long wifiConnectionTimeout = 30000; // 30 seconds timeout for WiFi connection bool pzemConnected = true; // Flag to track PZEM connection status // Initialize TFT and PZEM Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST); PZEM004Tv30 pzem(Serial2, PZEM_RX_PIN, PZEM_TX_PIN); // Structure for PZEM readings struct PzemData { float voltage; float current; float power; float energy; float frequency; float pf; bool isValid; }; // Store readings PzemData readings = {0, 0, 0, 0, 0, 0, false}; // Store IP address as a string to avoid repeated calls String ipAddressStr = ""; // Function prototypes void handleRoot(); void handleNotFound(); void handleData(); void handleReset(); void readPzemData(); void drawLayout(); void updateDisplayValues(); void showNotification(const String& message, uint16_t color, unsigned long duration = 3000); void clearNotification(); void displayStartupSequence(); void connectToWiFi(); void printDebugInfo(const char* action, bool success); void updateWebpageValues(); // Forward declaration // Web page stored in PROGMEM const char index_html[] PROGMEM = R"rawliteral( <!DOCTYPE HTML> <html lang="en"> <head> <title>ESP32 Power Monitor</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <style> :root { --primary-color: #3498db; --secondary-color: #2c3e50; --background-color: #f5f7fa; --card-background: #ffffff; --text-color: #333333; --text-light: #7f8c8d; --shadow: 0 4px 6px rgba(0, 0, 0, 0.1); --transition: all 0.3s ease; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--background-color); margin: 0; padding: 0; color: var(--text-color); line-height: 1.6; } .container { max-width: 1000px; margin: 0 auto; padding: 20px; } .header { background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); color: white; padding: 20px; text-align: center; margin-bottom: 30px; border-radius: 8px; box-shadow: var(--shadow); } .header h1 { margin: 0; font-size: 28px; display: flex; align-items: center; justify-content: center; } .header h1 i { margin-right: 10px; } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; } .card { background: var(--card-background); border-radius: 8px; padding: 20px; box-shadow: var(--shadow); transition: var(--transition); display: flex; flex-direction: column; align-items: center; } .card:hover { transform: translateY(-5px); box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15); } .icon-container { background-color: var(--primary-color); width: 60px; height: 60px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 15px; } .icon-container i { color: white; font-size: 24px; } .title { color: var(--text-light); font-size: 16px; margin-bottom: 8px; text-align: center; } .value { font-size: 28px; font-weight: bold; color: var(--secondary-color); text-align: center; } .unit { font-size: 16px; color: var(--text-light); margin-left: 2px; } .footer { text-align: center; margin-top: 30px; color: var(--text-light); font-size: 14px; padding: 10px; } .button-container { text-align: center; margin-top: 30px; } .button { background: linear-gradient(to right, var(--primary-color), var(--secondary-color)); color: white; border: none; padding: 12px 24px; border-radius: 30px; cursor: pointer; font-size: 16px; font-weight: bold; transition: var(--transition); display: inline-flex; align-items: center; box-shadow: var(--shadow); } .button i { margin-right: 8px; } .button:hover { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); } .button:active { transform: translateY(0); } .update-status { text-align: center; margin-top: 10px; color: var(--text-light); font-size: 14px; } /* Responsive adjustments */ @media (max-width: 768px) { .header h1 { font-size: 24px; } .grid { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); } } @media (max-width: 480px) { .card { padding: 15px; } .icon-container { width: 50px; height: 50px; } .value { font-size: 24px; } } </style> </head> <body> <div class="container"> <div class="header"> <h1><i class="fas fa-bolt"></i> ESP32 Power Monitor</h1> </div> <div class="grid"> <div class="card"> <div class="icon-container"> <i class="fas fa-plug"></i> </div> <div class="title">Voltage</div> <div class="value" id="voltage">--<span class="unit">V</span></div> </div> <div class="card"> <div class="icon-container"> <i class="fas fa-tachometer-alt"></i> </div> <div class="title">Current</div> <div class="value" id="current">--<span class="unit">A</span></div> </div> <div class="card"> <div class="icon-container"> <i class="fas fa-bolt"></i> </div> <div class="title">Power</div> <div class="value" id="power">--<span class="unit">W</span></div> </div> <div class="card"> <div class="icon-container"> <i class="fas fa-battery-three-quarters"></i> </div> <div class="title">Energy</div> <div class="value" id="energy">--<span class="unit">kWh</span></div> </div> <div class="card"> <div class="icon-container"> <i class="fas fa-wave-square"></i> </div> <div class="title">Frequency</div> <div class="value" id="frequency">--<span class="unit">Hz</span></div> </div> <div class="card"> <div class="icon-container"> <i class="fas fa-percentage"></i> </div> <div class="title">Power Factor</div> <div class="value" id="pf">--</div> </div> </div> <div class="button-container"> <button class="button" id="reset-btn"> <i class="fas fa-redo-alt"></i> Reset Energy Counter </button> </div> <div class="update-status" id="update-status"> Last updated: Just now </div> <div class="footer"> <i class="fas fa-microchip"></i> ESP32 Power Monitor with PZEM-004T v3.0 </div> </div> <script> function updateValues() { fetch('/data') .then(response => response.json()) .then(data => { const voltageElem = document.getElementById('voltage'); const currentElem = document.getElementById('current'); const powerElem = document.getElementById('power'); const energyElem = document.getElementById('energy'); const frequencyElem = document.getElementById('frequency'); const pfElem = document.getElementById('pf'); if (data.isValid) { voltageElem.textContent = data.voltage.toFixed(1) + ' V'; currentElem.textContent = data.current.toFixed(3) + ' A'; powerElem.textContent = data.power.toFixed(1) + ' W'; energyElem.textContent = data.energy.toFixed(3) + ' kWh'; frequencyElem.textContent = data.frequency.toFixed(1) + ' Hz'; pfElem.textContent = data.pf.toFixed(2); voltageElem.style.color = 'var(--secondary-color)'; // Reset color currentElem.style.color = 'var(--secondary-color)'; powerElem.style.color = 'var(--secondary-color)'; energyElem.style.color = 'var(--secondary-color)'; frequencyElem.style.color = 'var(--secondary-color)'; pfElem.style.color = 'var(--secondary-color)'; const now = new Date(); const timeString = now.toLocaleTimeString(); document.getElementById('update-status').textContent = `Last updated: ${timeString}`; } else { voltageElem.textContent = '-- V'; currentElem.textContent = '-- A'; powerElem.textContent = '-- W'; energyElem.textContent = '-- kWh'; frequencyElem.textContent = '-- Hz'; pfElem.textContent = '--'; voltageElem.style.color = 'red'; // Indicate no signal with red color currentElem.style.color = 'red'; powerElem.style.color = 'red'; energyElem.style.color = 'red'; frequencyElem.style.color = 'red'; pfElem.style.color = 'red'; document.getElementById('update-status').textContent = 'No Signal from Power Sensor'; } }) .catch(error => { console.error('Error:', error); document.getElementById('update-status').textContent = 'Update failed. Retrying...'; }); } document.getElementById('reset-btn').addEventListener('click', function() { this.disabled = true; this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Resetting...'; fetch('/reset', { method: 'POST' }) .then(response => response.json()) .then(data => { if (data.success) { updateValues(); this.innerHTML = '<i class="fas fa-check"></i> Reset Complete'; setTimeout(() => { this.disabled = false; this.innerHTML = '<i class="fas fa-redo-alt"></i> Reset Energy Counter'; }, 2000); } }) .catch(error => { console.error('Error:', error); this.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Reset Failed'; setTimeout(() => { this.disabled = false; this.innerHTML = '<i class="fas fa-redo-alt"></i> Reset Energy Counter'; }, 2000); }); }); // Initial update and set interval updateValues(); setInterval(updateValues, 2000); </script> </body> </html> )rawliteral"; void setup() { Serial.begin(115200); Serial.println("\n========================================"); Serial.println("PZEM-004T V3.0 Power Monitor"); Serial.println("========================================"); // Initialize Serial2 for PZEM Serial2.begin(9600, SERIAL_8N1, PZEM_RX_PIN, PZEM_TX_PIN); // Initialize TFT display tft.initR(INITR_BLACKTAB); tft.setRotation(3); // Landscape orientation tft.fillScreen(BLACK); // Display startup sequence displayStartupSequence(); // Connect to WiFi connectToWiFi(); // Setup web server routes server.on("/", handleRoot); server.on("/data", handleData); server.on("/reset", HTTP_POST, handleReset); server.onNotFound(handleNotFound); // Start server server.begin(); Serial.println("HTTP server started"); // Draw the initial display layout drawLayout(); } void loop() { // Check for serial commands if (Serial.available()) { char cmd = Serial.read(); if (cmd == 'r' || cmd == 'R') { bool success = pzem.resetEnergy(); if (success) { readings.energy = 0; printDebugInfo("Energy counter reset", true); showNotification("Energy counter reset", GREEN); updateWebpageValues(); // Update webpage after reset } else { printDebugInfo("Energy counter reset", false); showNotification("Reset failed", RED); } } } // Check WiFi connection and reconnect if needed if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi connection lost. Attempting to reconnect..."); connectToWiFi(); } // Read PZEM data at specified intervals unsigned long currentMillis = millis(); if (currentMillis - lastReadTime >= measurementInterval) { lastReadTime = currentMillis; readPzemData(); updateWebpageValues(); // Update webpage with new PZEM data } // Update display at refresh interval if (currentMillis - screenRefreshTime >= SCREEN_REFRESH_INTERVAL) { screenRefreshTime = currentMillis; updateDisplayValues(); // Clear notification if its time has expired if (notificationEndTime > 0 && currentMillis > notificationEndTime) { clearNotification(); } } // Handle web client requests server.handleClient(); } void printDebugInfo(const char* action, bool success) { Serial.print(action); Serial.print(": "); Serial.println(success ? "SUCCESS" : "FAILED"); } void displayStartupSequence() { tft.fillScreen(BLACK); // Display logo tft.setTextSize(2); tft.setTextColor(CYAN); tft.setCursor(25, 20); tft.println("PZEM-004T"); tft.setCursor(45, 40); tft.println("V3.0"); // Display loading bar tft.drawRect(20, 70, tft.width() - 40, 15, WHITE); for (int i = 0; i < tft.width() - 44; i++) { tft.fillRect(22, 72, i, 11, BLUE); delay(10); } tft.setTextSize(1); tft.setTextColor(GREEN); tft.setCursor(30, 100); tft.println("Power Monitor Initializing..."); delay(500); } void connectToWiFi() { // Only attempt to connect if not already connected if (WiFi.status() == WL_CONNECTED) { Serial.println("Already connected to WiFi"); return; } tft.fillScreen(BLACK); tft.setTextSize(1); tft.setTextColor(WHITE); tft.setCursor(10, 10); tft.println("Connecting to WiFi..."); tft.setCursor(10, 30); tft.print("SSID: "); tft.println(ssid); WiFi.begin(ssid, password); int dotCount = 0; tft.setCursor(10, 50); // Set timeout for connection unsigned long startAttempt = millis(); bool connected = false; while (millis() - startAttempt < wifiConnectionTimeout) { if (WiFi.status() == WL_CONNECTED) { connected = true; break; } delay(500); Serial.print("."); // Update dots on screen for connection progress tft.print("."); dotCount++; // Start a new line of dots if needed if (dotCount >= 30) { tft.setCursor(10, tft.getCursorY() + 10); dotCount = 0; } } if (connected) { // Store IP address as string to avoid repeated calls ipAddressStr = WiFi.localIP().toString(); tft.fillScreen(BLACK); tft.setCursor(10, 20); tft.setTextColor(GREEN); tft.println("WiFi Connected!"); tft.setCursor(10, 40); tft.println("IP Address:"); tft.setCursor(10, 55); tft.setTextColor(YELLOW); tft.println(ipAddressStr); Serial.println("\nWiFi connected successfully!"); Serial.print("IP address: "); Serial.println(ipAddressStr); } else { tft.fillScreen(BLACK); tft.setCursor(10, 20); tft.setTextColor(RED); tft.println("WiFi Connection Failed!"); tft.setCursor(10, 40); tft.println("Will continue without WiFi"); Serial.println("\nWiFi connection failed after timeout!"); Serial.println("Continuing without WiFi connectivity"); } delay(2000); } void readPzemData() { float newVoltage = pzem.voltage(); float newCurrent = pzem.current(); float newPower = pzem.power(); float newEnergy = pzem.energy(); float newFrequency = pzem.frequency(); float newPf = pzem.pf(); // Check if readings are valid bool newIsValid = !isnan(newVoltage) && !isnan(newCurrent) && !isnan(newPower) && !isnan(newEnergy) && !isnan(newFrequency) && !isnan(newPf); // If PZEM was connected and now it's not, show notification if (pzemConnected && !newIsValid) { showNotification("No Signal from PZEM!", RED, 5000); // Show for 5 seconds pzemConnected = false; // Update PZEM connection status } else if (!pzemConnected && newIsValid) { showNotification("PZEM Signal Restored!", GREEN, 3000); pzemConnected = true; // Update PZEM connection status } // Update readings readings.voltage = newVoltage; readings.current = newCurrent; readings.power = newPower; readings.energy = newEnergy; readings.frequency = newFrequency; readings.pf = newPf; readings.isValid = newIsValid; if (readings.isValid) { // Print values to Serial monitor with consistent formatting only if valid Serial.println("\n----- PZEM READINGS -----"); Serial.printf("Voltage: %.1f V\n", readings.voltage); Serial.printf("Current: %.3f A\n", readings.current); Serial.printf("Power: %.1f W\n", readings.power); Serial.printf("Energy: %.3f kWh\n", readings.energy); Serial.printf("Frequency: %.1f Hz\n", readings.frequency); Serial.printf("PF: %.2f\n", readings.pf); Serial.println("-------------------------"); } else { Serial.println("ERROR: Invalid PZEM readings - No Signal"); } } void drawLayout() { tft.fillScreen(BLACK); // Draw header bar tft.fillRect(0, 0, tft.width(), 14, BLUE); tft.setTextSize(1); tft.setTextColor(WHITE); tft.setCursor(5, 4); tft.print("PZEM POWER MONITOR"); // Draw measurement boxes (3 rows with 2 values each) // Row 1: Voltage & Current tft.fillRoundRect(2, 17, tft.width() - 4, 30, 3, DARKGRAY); tft.drawRoundRect(2, 17, tft.width() - 4, 30, 3, WHITE); // Row 2: Power & Energy tft.fillRoundRect(2, 49, tft.width() - 4, 30, 3, DARKGRAY); tft.drawRoundRect(2, 49, tft.width() - 4, 30, 3, WHITE); // Row 3: Frequency & Power Factor tft.fillRoundRect(2, 81, tft.width() - 4, 30, 3, DARKGRAY); tft.drawRoundRect(2, 81, tft.width() - 4, 30, 3, WHITE); // Draw labels for each row tft.setTextSize(1); tft.setTextColor(CYAN); // Row 1 labels tft.setCursor(5, 20); tft.print("Voltage:"); tft.setCursor(tft.width() / 2 + 5, 20); tft.print("Current:"); // Row 2 labels tft.setCursor(5, 52); tft.print("Power:"); tft.setCursor(tft.width() / 2 + 5, 52); tft.print("Energy:"); // Row 3 labels tft.setCursor(5, 84); tft.print("Frequency:"); tft.setCursor(tft.width() / 2 + 5, 84); tft.print("PF:"); // Draw notification area and IP address at bottom tft.drawRect(2, 113, tft.width() - 4, 14, WHITE); // Display IP address in footer tft.setTextSize(1); tft.setTextColor(GREEN); tft.setCursor(5, 130); tft.print("IP: "); // Use stored IP address string if (WiFi.status() == WL_CONNECTED) { tft.print(ipAddressStr); } else { tft.setTextColor(RED); tft.print("Not Connected"); } layoutDrawn = true; } void updateDisplayValues() { if (!layoutDrawn) { drawLayout(); } // Set text properties for values tft.setTextSize(1); // Display "NO SIGNAL" if readings are invalid if (!readings.isValid) { tft.fillRect(5, 30, tft.width() - 10, 10, DARKGRAY); tft.setTextColor(RED, DARKGRAY); tft.setCursor(5, 30); tft.print("NO SIGNAL"); tft.fillRect((tft.width() / 2) + 5, 30, (tft.width() / 2) - 10, 10, DARKGRAY); tft.fillRect(5, 62, (tft.width() / 2) - 10, 10, DARKGRAY); tft.fillRect((tft.width() / 2) + 5, 62, (tft.width() / 2) - 10, 10, DARKGRAY); tft.fillRect(5, 94, (tft.width() / 2) - 10, 10, DARKGRAY); tft.fillRect((tft.width() / 2) + 5, 94, (tft.width() / 2) - 10, 10, DARKGRAY); return; } // Format values as strings with appropriate decimal places char voltageStr[10], currentStr[10], powerStr[10], energyStr[15]; char frequencyStr[10], pfStr[10]; sprintf(voltageStr, "%.1f V", readings.voltage); sprintf(currentStr, "%.3f A", readings.current); sprintf(powerStr, "%.1f W", readings.power); sprintf(energyStr, "%.3f kWh", readings.energy); sprintf(frequencyStr, "%.1f Hz", readings.frequency); sprintf(pfStr, "%.2f", readings.pf); // Row 1: Voltage & Current // Clear previous values first tft.fillRect(5, 30, (tft.width() / 2) - 10, 10, DARKGRAY); tft.fillRect((tft.width() / 2) + 5, 30, (tft.width() / 2) - 10, 10, DARKGRAY); tft.setTextColor(YELLOW, DARKGRAY); tft.setCursor(5, 30); tft.print(voltageStr); tft.setCursor(tft.width() / 2 + 5, 30); tft.print(currentStr); // Row 2: Power & Energy // Clear previous values tft.fillRect(5, 62, (tft.width() / 2) - 10, 10, DARKGRAY); tft.fillRect((tft.width() / 2) + 5, 62, (tft.width() / 2) - 10, 10, DARKGRAY); tft.setCursor(5, 62); tft.print(powerStr); tft.setCursor(tft.width() / 2 + 5, 62); tft.print(energyStr); // Row 3: Frequency & Power Factor // Clear previous values tft.fillRect(5, 94, (tft.width() / 2) - 10, 10, DARKGRAY); tft.fillRect((tft.width() / 2) + 5, 94, (tft.width() / 2) - 10, 10, DARKGRAY); tft.setCursor(5, 94); tft.print(frequencyStr); // Color PF based on value if (readings.pf >= 0.95) { tft.setTextColor(GREEN, DARKGRAY); } else if (readings.pf >= 0.85) { tft.setTextColor(YELLOW, DARKGRAY); } else { tft.setTextColor(RED, DARKGRAY); } tft.setCursor(tft.width() / 2 + 5, 94); tft.print(pfStr); // Always update IP address in case WiFi status has changed tft.fillRect(5, 130, tft.width() - 10, 8, BLACK); tft.setTextSize(1); tft.setCursor(5, 130); tft.print("IP: "); if (WiFi.status() == WL_CONNECTED) { tft.setTextColor(GREEN); tft.print(ipAddressStr); } else { tft.setTextColor(RED); tft.print("Not Connected"); } } void handleRoot() { String html = FPSTR(index_html); server.send(200, "text/html", html); } void handleData() { // Create JSON response - make it more compact StaticJsonDocument<192> jsonDoc; jsonDoc["isValid"] = readings.isValid; // Always send isValid status // Only include sensor values if readings are valid if (readings.isValid) { jsonDoc["voltage"] = readings.voltage; jsonDoc["current"] = readings.current; jsonDoc["power"] = readings.power; jsonDoc["energy"] = readings.energy; jsonDoc["frequency"] = readings.frequency; jsonDoc["pf"] = readings.pf; } String response; serializeJson(jsonDoc, response); server.send(200, "application/json", response); } void handleReset() { bool success = pzem.resetEnergy(); // Create minimal JSON response StaticJsonDocument<32> jsonDoc; jsonDoc["success"] = success; if (success) { readings.energy = 0; showNotification("Energy counter reset", GREEN); printDebugInfo("Web reset energy counter", true); } else { showNotification("Reset failed", RED); printDebugInfo("Web reset energy counter", false); } String response; serializeJson(jsonDoc, response); server.send(200, "application/json", response); } void handleNotFound() { server.send(404, "text/plain", "File Not Found"); } void showNotification(const String& message, uint16_t color, unsigned long duration) { // Clear notification area tft.fillRect(3, 114, tft.width() - 6, 12, BLACK); // Display notification tft.setTextColor(color); tft.setTextSize(1); tft.setCursor(5, 117); tft.print(message); // Save notification details currentNotification = message; notificationColor = color; notificationEndTime = millis() + duration; } void clearNotification() { tft.fillRect(3, 114, tft.width() - 6, 12, BLACK); currentNotification = ""; notificationEndTime = 0; } // Function to update webpage values directly from ESP32 (if needed for more frequent updates) void updateWebpageValues() { if (WiFi.status() == WL_CONNECTED) { // Prepare JSON data dynamically here if needed for push updates (e.g., using WebSockets if higher frequency is really necessary) // For simple frequent updates, the current pull-based approach (JavaScript fetch) every 2 seconds is usually sufficient and simpler. // If you were to implement push updates, you would need to manage WebSocket connections and send data to connected clients. // For this example, we'll leave it as a placeholder. // Consider using WebSockets for real-time push if truly needed for very high frequency updates. // For now, the 2-second pull update is quite frequent for power monitoring. } } |
Testing and Troubleshooting Your ESP32 IoT Energy Meter
After uploading the complete code, connect the PZEM-004T to the AC mains for live measurements. Open the Arduino Serial Monitor (115200 baud) to view sensor data. You can also see the display.
The display shows an initialization message while the system starts up. components
WiFi Connection: The system connects to your configured WiFi network
The display shows the connecting status while establishing a WiFi connection.
Connected Status: After a successful connection, the system displays its IP address; the display shows the IP address that you’ll use to access the web dashboard
Data Display: The TFT display shows real-time readings
- All parameters update every 2 seconds
- The display shows voltage, current, power, energy, frequency, and power factor.
Testing with Different Loads:
- No Load Testing: Connect the system without any appliance to verify baseline readings; the display shows near-zero current and power readings when no load is connected
- Small Load (2W Smart Bulb):
The display shows a small current draw and accurate power measurement with minimal Load.
Medium Load (100W Bulb):
The display clearly shows increased current and power consumption with the 100W Load.
Web Dashboard Access: On a device connected to the same WiFi network, enter the ESP32’s IP address in a web browser
- The dashboard updates in real-time using AJAX
- The interface is responsive and works on both desktop and mobile browsers
Long-term Testing:
- Run the system for at least 24 hours
- Check for stability issues
- Verify automatic WiFi reconnection after network interruptions
Conclusion
Your IoT Energy Meter with ESP32 and PZEM-004T is now fully functional! This system provides complete energy monitoring through a local TFT display and a web-based dashboard. The real-time voltage, current, power, energy consumption, frequency, and power factor measurements make this a perfect tool for home energy management.
Consider adding a small enclosure and proper isolation for safety, especially when working with AC mains, for improved stability in long-term use.
This project is perfect for those looking to monitor their energy usage and develop energy-saving strategies.