Giám sát điện trong nhà bằng ESP32 và nhiều PZEM-004T: từng nhánh, quá tải, quên tắt, công suất nền

Dùng ESP32 đọc nhiều PZEM-004T chung một đường Modbus để giám sát điện từng nhánh: cảnh báo quá tải, điện áp thấp, bình nóng lạnh quên tắt, điện năng từng ngày, công suất nền.

Hóa đơn tiền điện chỉ cho biết cả tháng dùng bao nhiêu. Muốn biết thiết bị nào ngốn điện, lúc nào nhà gần quá tải, điện áp có bị sụt giờ cao điểm hay ai đó quên tắt bình nóng lạnh, cần đo theo từng nhánh và theo thời gian thực. Bài này làm một bộ giám sát điện trong nhà bằng ESP32 và nhiều module PZEM-004T v3 nối chung một đường dây TTL: mỗi PZEM đo một nhánh (tổng, điều hòa, bình nóng lạnh), ESP32 ghi điện năng từng ngày, đo công suất nền ban đêm, cảnh báo và hiện tất cả trên một trang web.

Bài này nối tiếp bài PZEM-004T (đọc một module qua Modbus) và bài đo điện năng, tính tiền điện bậc thang trong cùng loạt. Ở đây trọng tâm là nhiều nhánh và cảnh báo, không tính tiền.

1. An toàn trước tiên

PZEM-004T có phần nối trực tiếp với điện 220 V. Phần trong tủ điện (đấu dây đo áp, luồn vòng CT) phải cắt CB tổng trước khi làm và nên nhờ thợ điện. PZEM và ESP32 đặt trong hộp nhựa cách điện, không chạm vào khi đang có điện. Phía TTL của PZEM cách ly với 220 V bằng opto bên trong module, nên ESP32 và dây tín hiệu an toàn khi module được lắp đúng.

2. Đặt PZEM theo từng nhánh

Sơ đồ tủ điện: công tơ, CB tổng có vòng CT 1 nối PZEM địa chỉ 1 đo nhánh tổng; nhánh điều hòa có CT 2 nối PZEM địa chỉ 2; nhánh bình nóng lạnh có CT 3 nối PZEM địa chỉ 3; các PZEM nối chung đường TTL tới ESP32
Một PZEM ở nhánh tổng, các PZEM khác ở thiết bị lớn (hình minh họa).
  • Nhánh tổng (sau CB tổng): biết cả nhà đang dùng bao nhiêu, điện áp lưới, dùng để cảnh báo quá tải.
  • Nhánh riêng cho thiết bị công suất lớn hoặc hay bị quên: điều hòa, bình nóng lạnh, bếp từ, máy bơm.
  • Phần còn lại (tổng trừ các nhánh riêng) là đèn, tủ lạnh, tivi, ổ cắm. Chương trình dùng phần này để đo công suất nền ban đêm.
  • Vòng CT của PZEM-004T 100 A là loại kẹp, chỉ luồn một dây pha (L) qua vòng. Luồn cả L và N thì dòng hai chiều triệt tiêu, số đo gần bằng 0.

3. Nối nhiều PZEM vào một ESP32

Ba PZEM nối song song vào bốn đường 5V, GND, TX vào GPIO16, RX từ GPIO17 của ESP32
Bốn dây chung cho mọi PZEM: 5V, GND, TX, RX.

Modbus RTU cho phép nhiều thiết bị dùng chung một đường truyền: ESP32 gửi câu hỏi kèm địa chỉ, chỉ thiết bị có đúng địa chỉ đó trả lời. Vì vậy TX của mọi PZEM nối chung vào GPIO16, RX nối chung vào GPIO17. Hai, ba module mắc song song thường chạy ổn; nhiều module hơn hoặc dây dài thì nên đo lại dạng tín hiệu, hoặc chuyển sang mạch RS-485.

4. Đặt địa chỉ cho từng PZEM

PZEM mới xuất xưởng đều có địa chỉ 1. Trước khi nối chung, đặt địa chỉ cho từng module:

  1. Nạp chương trình, chỉ nối một PZEM vào ESP32 (phía 220 V cần có điện để PZEM chạy).
  2. Mở Serial Monitor tốc độ 115200, chọn kiểu xuống dòng Newline, gõ A 2 rồi Enter. Serial báo Da dat dia chi PZEM = 2.
  3. Làm lần lượt với module thứ ba (A 3). Module đo nhánh tổng giữ địa chỉ 1.
Khung Modbus đặt địa chỉ F8 06 00 02 00 02 BD A2; khung đọc 10 thanh ghi cho địa chỉ 1, 2, 3: 01 04 00 00 00 0A 70 0D, 02 04 00 00 00 0A 70 3E, 03 04 00 00 00 0A 71 EF
Khung đặt địa chỉ và khung hỏi từng PZEM; CRC tính đúng như chương trình.

Lệnh đặt địa chỉ gửi tới địa chỉ chung 0xF8: mọi PZEM trên đường dây đều nhận và làm theo, nên chỉ được gõ khi đang nối đúng một module.

5. Chuẩn bị

6. Chương trình

Sửa bảng NHANH[] theo nhà mình: địa chỉ, tên (không dấu), ngưỡng quá tải, thời gian chạy liên tục tối đa. Ngưỡng quá tải nhánh tổng đặt thấp hơn dòng CB tổng: CB 32 A ở 220 V tương ứng khoảng 7 kW, đặt cảnh báo khoảng 6 kW là vừa.

// PYWORLD.VN - Giám sát điện trong nhà bằng ESP32 và nhiều PZEM-004T v3 chung một đường TTL: từng nhánh, cảnh báo, trang web
// PYWORLD.VN - PZEM TX vào GPIO16 (RX2), RX vào GPIO17 (TX2), 5V và GND chung; đặt địa chỉ từng PZEM bằng lệnh "A n" khi chỉ nối một module
// PYWORLD.VN - Cảnh báo quá tải, điện áp ngoài ngưỡng, thiết bị chạy quá lâu; điện năng từng ngày, công suất nền ban đêm

#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <time.h>
#include <stdarg.h>

const char *TEN_WIFI = "TEN_WIFI";
const char *MAT_KHAU_WIFI = "MAT_KHAU_WIFI";
const int CHAN_RX2 = 16;
const int CHAN_TX2 = 17;
const float AP_THAP = 200, AP_CAO = 240;   // ngưỡng cảnh báo điện áp ở nhánh tổng
const float NGUONG_CHAY_W = 30;            // trên mức này coi như thiết bị trong nhánh đang chạy
const uint32_t CHU_KY_DOC_MS = 2000;
const int SO_SU_KIEN = 20;

// Các struct khai báo trước mọi hàm.
struct Nhanh { uint8_t diaChi; const char *ten; float congSuatMax; uint16_t chayToiDaPhut; };
struct SoDo { float dienAp, dong, congSuat, tanSo, heSoCongSuat; uint32_t dienNangWh; };
struct TrangThai { SoDo s; bool doc, coDauNgay, baoQuaTai, baoChayLau; uint32_t whDauNgay; time_t chayTu; };
struct SuKien { time_t luc; char noiDung[96]; };

// Mỗi nhánh một PZEM, địa chỉ khác nhau. chayToiDaPhut = 0: không cảnh báo chạy lâu.
const Nhanh NHANH[] = {
  {1, "Tong", 6000, 0},
  {2, "Dieu hoa", 1600, 0},
  {3, "Binh nong lanh", 3000, 90},
};
const int SO_NHANH = sizeof(NHANH) / sizeof(NHANH[0]);

HardwareSerial pzem(2);
WebServer may(80);
Preferences boNho;
TrangThai tt[SO_NHANH];
SuKien suKien[SO_SU_KIEN];
int soSuKien = 0, viTriSuKien = -1;
int ngayDaChot = -1;                       // năm * 1000 + thứ tự ngày trong năm
bool baoAp = false;
float nenDangDo = NAN, congSuatNen = NAN;

// ---- Modbus RTU tới PZEM (như bài PZEM-004T) ----
uint16_t crcModbus(const uint8_t *du, size_t n) {
  uint16_t crc = 0xFFFF;
  for (size_t i = 0; i < n; i++) {
    crc ^= du[i];
    for (int b = 0; b < 8; b++) crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
  }
  return crc;
}

bool traoDoi(uint8_t *gui, size_t n, uint8_t *nhan, size_t canNhan) {
  uint16_t crc = crcModbus(gui, n);
  gui[n] = crc & 0xFF;
  gui[n + 1] = crc >> 8;
  while (pzem.available()) pzem.read();
  pzem.write(gui, n + 2);
  size_t da = 0;
  uint32_t batDau = millis();
  while (da < canNhan && millis() - batDau < 200) {
    if (pzem.available()) nhan[da++] = pzem.read();
  }
  if (da < canNhan) return false;
  return crcModbus(nhan, canNhan - 2) == (uint16_t)(nhan[canNhan - 2] | (nhan[canNhan - 1] << 8));
}

uint16_t tu(const uint8_t *p) { return (p[0] << 8) | p[1]; }
uint32_t tu32(const uint8_t *p) { return tu(p) | ((uint32_t)tu(p + 2) << 16); }

bool docPzem(uint8_t diaChi, SoDo &s) {
  uint8_t gui[8] = {diaChi, 0x04, 0x00, 0x00, 0x00, 0x0A};
  uint8_t nhan[25];
  if (!traoDoi(gui, 6, nhan, 25) || nhan[0] != diaChi || nhan[1] != 0x04 || nhan[2] != 20) return false;
  const uint8_t *d = nhan + 3;
  s.dienAp = tu(d) * 0.1f;
  s.dong = tu32(d + 2) * 0.001f;
  s.congSuat = tu32(d + 6) * 0.1f;
  s.dienNangWh = tu32(d + 10);
  s.tanSo = tu(d + 14) * 0.1f;
  s.heSoCongSuat = tu(d + 16) * 0.01f;
  return true;
}

// Ghi thanh ghi 0x0002 (địa chỉ Modbus) của module duy nhất đang nối, gửi tới địa chỉ chung 0xF8.
bool datDiaChi(uint8_t moi) {
  uint8_t gui[8] = {0xF8, 0x06, 0x00, 0x02, 0x00, moi};
  uint8_t nhan[8];
  return traoDoi(gui, 6, nhan, 8) && nhan[1] == 0x06 && nhan[5] == moi;
}

// ---- Sự kiện ----
void ghiSuKien(const char *dang, ...) {
  viTriSuKien = (viTriSuKien + 1) % SO_SU_KIEN;
  if (soSuKien < SO_SU_KIEN) soSuKien++;
  SuKien &e = suKien[viTriSuKien];
  e.luc = time(nullptr);
  va_list ds;
  va_start(ds, dang);
  vsnprintf(e.noiDung, sizeof(e.noiDung), dang, ds);
  va_end(ds);
  struct tm t;
  localtime_r(&e.luc, &t);
  Serial.printf("[%02d:%02d] %s\n", t.tm_hour, t.tm_min, e.noiDung);
}

// ---- Điện năng theo ngày ----
// Số đầu ngày của nhánh i: khởi động lại trong ngày thì lấy số đã lưu, không thì lấy số hiện tại của PZEM.
void layDauNgay(int i, bool homNayDaLuu) {
  char khoa[8];
  snprintf(khoa, sizeof(khoa), "wh%d", i);
  if (homNayDaLuu && boNho.isKey(khoa)) {
    tt[i].whDauNgay = boNho.getUInt(khoa, tt[i].s.dienNangWh);
  } else {
    tt[i].whDauNgay = tt[i].s.dienNangWh;
    boNho.putUInt(khoa, tt[i].whDauNgay);
  }
  tt[i].coDauNgay = true;
}

void chotNgay(const struct tm &t) {
  static bool homNayDaLuu = false;
  int ngay = (t.tm_year + 1900) * 1000 + t.tm_yday;
  if (ngay != ngayDaChot) {
    for (int i = 0; i < SO_NHANH; i++) {
      if (ngayDaChot >= 0 && tt[i].coDauNgay) ghiSuKien("%s hom qua: %.2f kWh", NHANH[i].ten, (tt[i].s.dienNangWh - tt[i].whDauNgay) / 1000.0f);
      tt[i].coDauNgay = false;
    }
    homNayDaLuu = boNho.getInt("ngay", -1) == ngay;
    boNho.putInt("ngay", ngay);
    ngayDaChot = ngay;
  }
  for (int i = 0; i < SO_NHANH; i++) {
    if (tt[i].doc && !tt[i].coDauNgay) layDauNgay(i, homNayDaLuu);
  }
}

// Công suất nền: nhỏ nhất của phần còn lại (tổng trừ các nhánh riêng) từ 1 giờ tới 5 giờ sáng,
// lúc thiết bị dùng điện thường xuyên đã tắt; phần còn lại là những thứ cắm điện suốt ngày đêm.
void theoDoiNen(const struct tm &t, float w) {
  if (t.tm_hour >= 1 && t.tm_hour < 5) {
    nenDangDo = isnan(nenDangDo) ? w : min(nenDangDo, w);
  } else if (!isnan(nenDangDo)) {
    congSuatNen = nenDangDo;
    nenDangDo = NAN;
    ghiSuKien("Cong suat nen dem qua %.0f W (ngoai cac nhanh rieng), khoang %.0f kWh/thang", congSuatNen, congSuatNen * 24 * 30 / 1000);
  }
}

void kiemTra(int i, time_t bay) {
  TrangThai &a = tt[i];
  const Nhanh &n = NHANH[i];
  if (a.s.congSuat > n.congSuatMax && !a.baoQuaTai) {
    a.baoQuaTai = true;
    ghiSuKien("QUA TAI %s: %.0f W (nguong %.0f W)", n.ten, a.s.congSuat, n.congSuatMax);
  } else if (a.s.congSuat < 0.9f * n.congSuatMax) {
    a.baoQuaTai = false;
  }
  if (a.s.congSuat > NGUONG_CHAY_W) {
    if (!a.chayTu) a.chayTu = bay;
    if (n.chayToiDaPhut && !a.baoChayLau && bay - a.chayTu >= (time_t)n.chayToiDaPhut * 60) {
      a.baoChayLau = true;
      ghiSuKien("%s chay lien tuc %u phut: co the quen tat", n.ten, (unsigned)n.chayToiDaPhut);
    }
  } else {
    a.chayTu = 0;
    a.baoChayLau = false;
  }
}

void docTatCa() {
  time_t bay = time(nullptr);
  struct tm t;
  bool coGio = getLocalTime(&t, 0);
  for (int i = 0; i < SO_NHANH; i++) {
    SoDo s;
    bool doc = docPzem(NHANH[i].diaChi, s);
    if (!doc && tt[i].doc) ghiSuKien("Mat ket noi PZEM dia chi %u (%s)", NHANH[i].diaChi, NHANH[i].ten);
    if (doc && !tt[i].doc) ghiSuKien("Doc duoc PZEM dia chi %u (%s)", NHANH[i].diaChi, NHANH[i].ten);
    tt[i].doc = doc;
    if (!doc) continue;
    if (s.dienNangWh < tt[i].s.dienNangWh) tt[i].whDauNgay = s.dienNangWh;   // bộ đếm của PZEM vừa bị xóa
    tt[i].s = s;
    kiemTra(i, bay);
  }
  if (!tt[0].doc) return;
  float v = tt[0].s.dienAp;
  if (!baoAp && (v < AP_THAP || v > AP_CAO)) {
    baoAp = true;
    ghiSuKien("Dien ap bat thuong: %.1f V", v);
  } else if (baoAp && v > AP_THAP + 2 && v < AP_CAO - 2) {
    baoAp = false;
    ghiSuKien("Dien ap tro lai binh thuong: %.1f V", v);
  }
  if (coGio) {
    chotNgay(t);
    float conLai = tt[0].s.congSuat;
    for (int i = 1; i < SO_NHANH; i++) {
      if (tt[i].doc) conLai -= tt[i].s.congSuat;
    }
    theoDoiNen(t, max(0.0f, conLai));
  }
}

// ---- Trang web ----
const char TRANG_WEB[] = R"rawliteral(<!doctype html><html lang="vi"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><title>Giám sát điện</title>
<style>body{font-family:sans-serif;margin:14px;background:#f4f6f9;color:#1d2b3a}table{border-collapse:collapse;width:100%;background:#fff}
td,th{padding:8px;border-bottom:1px solid #e3e8ef;text-align:right}td:first-child,th:first-child{text-align:left}
.w{font-weight:700;font-size:18px}.loi{color:#c92a2a}ul{background:#fff;padding:10px 26px;border-radius:8px}</style></head><body>
<h2>Giám sát điện trong nhà</h2><p id="gio"></p>
<table><thead><tr><th>Nhánh</th><th>V</th><th>A</th><th>W</th><th>PF</th><th>kWh hôm nay</th><th>Đang chạy</th></tr></thead><tbody id="b"></tbody></table>
<p id="nen"></p><h3>Sự kiện gần đây</h3><ul id="sk"></ul>
<script>
function tai(){fetch('/du-lieu').then(function(r){return r.json();}).then(function(j){
document.getElementById('gio').textContent='Cập nhật lúc '+j.gio;var h='';
j.nhanh.forEach(function(n){h+=n.ok?'<tr><td>'+n.ten+'</td><td>'+n.v.toFixed(1)+'</td><td>'+n.a.toFixed(2)+'</td><td class="w">'+n.w.toFixed(0)+
'</td><td>'+n.pf.toFixed(2)+'</td><td>'+n.kwh.toFixed(2)+'</td><td>'+(n.chay>0?n.chay+' phút':'')+'</td></tr>':
'<tr><td>'+n.ten+'</td><td class="loi" colspan="6">không đọc được PZEM</td></tr>';});
document.getElementById('b').innerHTML=h;
document.getElementById('nen').textContent=j.nen===null?'Công suất nền: đo từ 1 giờ tới 5 giờ sáng':'Công suất nền đêm qua: '+j.nen.toFixed(0)+' W';
document.getElementById('sk').innerHTML=j.sk.map(function(e){return '<li>'+e[0]+': '+e[1]+'</li>';}).join('');});}
tai();setInterval(tai,3000);
</script></body></html>)rawliteral";

void trangChu() {
  may.send(200, "text/html; charset=utf-8", TRANG_WEB);
}

void duLieu() {
  time_t bay = time(nullptr);
  char dem[48];
  struct tm t;
  localtime_r(&bay, &t);
  strftime(dem, sizeof(dem), "%H:%M:%S %d/%m/%Y", &t);
  String s;
  s.reserve(3000);
  s += "{\"gio\":\"";
  s += dem;
  s += "\",\"nen\":";
  s += isnan(congSuatNen) ? String("null") : String(congSuatNen, 0);
  s += ",\"nhanh\":[";
  for (int i = 0; i < SO_NHANH; i++) {
    const TrangThai &a = tt[i];
    snprintf(dem, sizeof(dem), "%s{\"ten\":\"", i ? "," : "");
    s += dem;
    s += NHANH[i].ten;
    s += "\",\"ok\":";
    s += a.doc ? "true" : "false";
    if (a.doc) {
      char so[160];
      snprintf(so, sizeof(so), ",\"v\":%.1f,\"a\":%.3f,\"w\":%.1f,\"pf\":%.2f,\"kwh\":%.3f,\"chay\":%ld",
               a.s.dienAp, a.s.dong, a.s.congSuat, a.s.heSoCongSuat, a.coDauNgay ? (a.s.dienNangWh - a.whDauNgay) / 1000.0f : 0.0f,
               (i > 0 && a.chayTu) ? (long)((bay - a.chayTu) / 60) : 0L);   // nhánh tổng luôn có tải nên không đếm
      s += so;
    }
    s += "}";
  }
  s += "],\"sk\":[";
  for (int k = 0; k < soSuKien; k++) {     // mới nhất trước
    const SuKien &e = suKien[(viTriSuKien - k + SO_SU_KIEN) % SO_SU_KIEN];
    localtime_r(&e.luc, &t);
    strftime(dem, sizeof(dem), "%H:%M %d/%m", &t);
    s += k ? ",[\"" : "[\"";
    s += dem;
    s += "\",\"";
    s += e.noiDung;
    s += "\"]";
  }
  s += "]}";
  may.send(200, "application/json", s);
}

void lenhSerial() {
  if (!Serial.available()) return;
  String lenh = Serial.readStringUntil('\n');
  lenh.trim();
  int so;
  if (sscanf(lenh.c_str(), "A %d", &so) == 1 && so >= 1 && so <= 247) {
    Serial.printf(datDiaChi(so) ? "Da dat dia chi PZEM = %d\n" : "Khong dat duoc dia chi %d (chi noi mot PZEM)\n", so);
  } else if (lenh.length()) {
    Serial.println("Lenh: A n  dat dia chi n (1-247) cho PZEM dang noi mot minh");
  }
}

void setup() {
  Serial.begin(115200);
  pzem.begin(9600, SERIAL_8N1, CHAN_RX2, CHAN_TX2);
  boNho.begin("diennha", false);
  WiFi.mode(WIFI_STA);
  WiFi.begin(TEN_WIFI, MAT_KHAU_WIFI);
  configTzTime("ICT-7", "pool.ntp.org", "time.google.com");
  for (int i = 0; i < 30 && WiFi.status() != WL_CONNECTED; i++) delay(500);
  if (WiFi.status() == WL_CONNECTED) Serial.printf("Mo trinh duyet: http://%s\n", WiFi.localIP().toString().c_str());
  else Serial.println("Chua ket noi Wi-Fi: van doc PZEM va in canh bao ra Serial");
  for (int i = 0; i < SO_NHANH; i++) tt[i].doc = true;   // để lần đọc đầu báo nhánh nào không đọc được
  may.on("/", trangChu);
  may.on("/du-lieu", duLieu);
  may.begin();
  Serial.printf("Giam sat %d nhanh. Go \"A n\" de dat dia chi cho PZEM moi\n", SO_NHANH);
}

void loop() {
  may.handleClient();
  lenhSerial();
  static uint32_t lanDoc = 0;
  if (millis() - lanDoc >= CHU_KY_DOC_MS) {
    lanDoc = millis();
    docTatCa();
  }
}

Giải thích

  • Đọc từng nhánh: mỗi 2 giây, docTatCa() hỏi lần lượt các địa chỉ trong NHANH[] bằng hàm Modbus 0x04 (10 thanh ghi: điện áp, dòng, công suất, điện năng, tần số, hệ số công suất). Phần Modbus giống bài PZEM-004T, chỉ thêm tham số địa chỉ.
  • Quá tải: công suất vượt ngưỡng thì ghi một sự kiện; phải xuống dưới 90 % ngưỡng mới coi là hết quá tải, tránh báo liên tục khi công suất dao động quanh ngưỡng.
  • Chạy quá lâu: nhánh có công suất trên 30 W liên tục quá chayToiDaPhut thì báo "có thể quên tắt". Bình nóng lạnh thường chỉ cần 20–40 phút để nóng nước.
  • Điện áp (đo ở nhánh tổng): dưới 200 V hoặc trên 240 V thì báo; phải trở về trong khoảng 202–238 V mới báo bình thường.
  • Điện năng từng ngày: bộ đếm của PZEM tăng mãi; đầu mỗi ngày chương trình lưu số đếm vào Preferences, điện năng hôm nay là số hiện tại trừ số đầu ngày. Khởi động lại giữa ngày thì đọc lại số đã lưu. Sang ngày mới thì ghi sự kiện điện năng hôm qua của từng nhánh.
  • Công suất nền: từ 1 giờ tới 5 giờ sáng, lấy giá trị nhỏ nhất của phần còn lại (tổng trừ các nhánh riêng). Đó là những thiết bị cắm điện suốt ngày đêm: modem, camera, đồng hồ của lò vi sóng, sạc để quên trên ổ cắm, tủ lạnh lúc nghỉ.
  • Giờ lấy từ Internet theo múi giờ Việt Nam; 20 sự kiện gần nhất giữ trong RAM để trang web hiện ra.

7. Kết quả

Hình dưới là một ngày dùng điện theo mô hình: sáng đun bình nóng lạnh, trưa bật điều hòa, tối nấu ăn. Ngày này bình nóng lạnh bật lúc 19 giờ rồi bị quên tới 21 giờ 10, đúng lúc bếp từ và ấm siêu tốc cùng chạy.

Đồ thị công suất một ngày của nhánh tổng, điều hòa, bình nóng lạnh và đồ thị điện áp; lúc 19:32 nhánh tổng vượt ngưỡng 6 kW và điện áp xuống dưới 200 V, lúc 20:30 bình nóng lạnh chạy liên tục 90 phút
Một ngày dùng điện và lúc chương trình ghi cảnh báo (mô hình minh họa).
Serial Monitor: 19:32 quá tải tổng 7206 W, điện áp bất thường 198,3 V; 19:40 điện áp trở lại 205,2 V; 20:30 bình nóng lạnh chạy liên tục 90 phút; 00:00 điện năng hôm qua của từng nhánh; 05:00 công suất nền đêm qua 46 W, khoảng 33 kWh mỗi tháng
Các sự kiện in ra Serial Monitor (theo mô hình minh họa).
Trang web giám sát: bảng điện áp, dòng, công suất, hệ số công suất, điện năng hôm nay và thời gian đang chạy của ba nhánh; công suất nền đêm qua; danh sách sự kiện gần đây
Trang web lúc 20:45 (số liệu mô hình minh họa).

8. Đọc số liệu để tiết kiệm điện

  • Công suất nền nhỏ nhưng chạy 24 giờ: 46 W là khoảng 33 kWh (33 số điện) mỗi tháng. Rút sạc, tắt hẳn thiết bị chờ là cách giảm dễ nhất.
  • Điện năng từng nhánh mỗi ngày cho biết thiết bị nào chiếm phần lớn hóa đơn. Mùa nóng thường là điều hòa; mùa lạnh ở miền Bắc là bình nóng lạnh.
  • Điện áp sụt giờ cao điểm: dưới khoảng 200 V thì điều hòa, tủ lạnh khó khởi động, bóng đèn tối. Ghi lại thời điểm, điện áp để báo cho điện lực nếu tình trạng lặp lại nhiều ngày.
  • Hệ số công suất (PF) thấp ở nhánh có động cơ, đèn LED giá rẻ là bình thường; hóa đơn hộ gia đình tính theo kWh, không tính phần công suất phản kháng.

9. Lỗi hay gặp

Hiện tượngNguyên nhân thường gặpCách xử lý
"Mat ket noi PZEM dia chi 2"Chưa đặt địa chỉ, hai module trùng địa chỉ, phía 220 V của module đó mất điệnNối riêng từng module, gõ lại A n; PZEM chỉ trả lời khi phía 220 V có điện
"Khong dat duoc dia chi"Đang nối nhiều module, sai dây TX/RXChỉ nối một module; đổi chéo TX, RX
Dòng của nhánh gần bằng 0 dù tải đang chạyLuồn cả dây pha và dây trung tính qua CTChỉ luồn dây pha
Tổng nhỏ hơn tổng các nhánh riêngCT tổng đặt sai chỗ, không bao hết các nhánhCT tổng phải ở sau CB tổng, trước khi chia nhánh
Điện năng hôm nay bị nhảy về 0Bộ đếm của PZEM vừa bị xóaChương trình tự lấy lại số đầu ngày; tránh xóa bộ đếm giữa ngày
Giờ trên trang web saiChưa kết nối Internet nên chưa lấy được giờKiểm tra Wi-Fi; điện năng theo ngày chỉ bắt đầu khi có giờ

10. Xem thêm


Tin tức liên quan

ESP32 với Firebase Realtime Database qua REST: đăng nhập ẩn danh, làm mới token, điều khiển relay
ESP32 với Firebase Realtime Database qua REST: đăng nhập ẩn danh, làm mới token, điều khiển relay

ESP32 dùng Firebase Realtime Database không cần thư viện Firebase: đăng nhập ẩn danh, tự làm mới token, đọc lệnh relay, ghi trạng thái và lịch sử, Rules theo uid.

Cấu hình LoRa trên ESP32: SF, băng thông, tỉ lệ mã, công suất và cách thử tầm xa
Cấu hình LoRa trên ESP32: SF, băng thông, tỉ lệ mã, công suất và cách thử tầm xa

Giải thích bốn tham số cấu hình LoRa (SF, băng thông, tỉ lệ mã, công suất), cách tính thời gian phát một gói và hướng dẫn làm bộ thử tầm xa bằng ESP32, SX1278 và màn hình OLED.

Hướng dẫn dùng màn hình OLED 1.3 inch SH1106 với ESP32
Hướng dẫn dùng màn hình OLED 1.3 inch SH1106 với ESP32

Hướng dẫn nối màn hình OLED 1.3 inch SH1106 với ESP32 DevKit V1 qua I2C, cài thư viện Adafruit SH110X, chạy code hiển thị chữ và xử lý lỗi thường gặp.

Đế ra chân ESP32 30 chân: cấp nguồn DC 6,5–16 V, hàng chân G V S và jumper chọn 5 V hay 3,3 V
Đế ra chân ESP32 30 chân: cấp nguồn DC 6,5–16 V, hàng chân G V S và jumper chọn 5 V hay 3,3 V

Cách dùng đế ra chân ESP32 30 chân: cấu tạo, cắm ESP32 đúng chiều, cấp nguồn jack DC 6,5–16 V và tính nhiệt IC ổn áp, hàng G V S, jumper 5V/3.3V, ví dụ relay và nút nhấn.

Hướng dẫn đo điện áp 16 bit bằng module ADS1115 với ESP32
Hướng dẫn đo điện áp 16 bit bằng module ADS1115 với ESP32

Hướng dẫn nối module ADC 16 bit ADS1115 với ESP32 DevKit V1 qua I2C, cài thư viện Adafruit ADS1X15, đọc 4 kênh ra vôn, đo vi sai và chọn gain an toàn.

ESP-NOW nhiều node với ESP32: nhiều mạch gửi về một trạm thu, hiển thị trên OLED
ESP-NOW nhiều node với ESP32: nhiều mạch gửi về một trạm thu, hiển thị trên OLED

Hướng dẫn dựng mô hình ESP-NOW nhiều node: tối đa bốn ESP32 gửi số đo về một trạm thu, trạm thu hiện điện áp, RSSI và tình trạng từng node trên màn hình OLED 0.96 inch.

LCD I2C không hiện chữ, chỉ hiện ô vuông, sai địa chỉ 0x27/0x3F: tìm lỗi từng bước
LCD I2C không hiện chữ, chỉ hiện ô vuông, sai địa chỉ 0x27/0x3F: tìm lỗi từng bước

LCD 1602, LCD 2004 I2C không hiện chữ, chỉ hiện hàng ô vuông đen hoặc sai địa chỉ 0x27/0x3F: tìm lỗi nguồn, tương phản, dây I2C, địa chỉ và chương trình kiểm tra tự động.

Hướng dẫn sử dụng màn hình LCD 1602 I2C với Arduino Uno
Hướng dẫn sử dụng màn hình LCD 1602 I2C với Arduino Uno

Hướng dẫn nối màn hình LCD 1602 I2C với Arduino Uno, cài thư viện LiquidCrystal_I2C, chạy code hiển thị và quét địa chỉ I2C 0x27/0x3F.


Bình luận
Đã thêm vào giỏ hàng