Đưa ESP32 và relay 4 kênh vào Home Assistant bằng MQTT discovery, không viết YAML
- 1. ESP32, máy chủ MQTT và Home Assistant
- 2. Chuẩn bị
- 3. Sơ đồ nối
- 4. Cài máy chủ MQTT và tài khoản trên Home Assistant
- 5. Chương trình
- 6. Giải thích các phần chính
- Tin cấu hình discovery
- Trạng thái kết nối và di chúc (last will)
- Giữ lại trạng thái, gửi lại cấu hình khi Home Assistant khởi động
- Sau khi mất điện
- 7. Kết quả
- 8. Lưu ý
- 9. Xử lý lỗi thường gặp
- 10. Xem thêm
ESP32 tự gửi tin cấu hình MQTT discovery, Home Assistant tạo 4 công tắc relay và cảm biến Wi-Fi; Mosquitto, tài khoản, di chúc, retain và bảng xử lý lỗi.
Home Assistant là phần mềm nhà thông minh mã nguồn mở, chạy trên máy trong nhà (Raspberry Pi, máy tính mini, máy ảo), gom thiết bị của nhiều hãng vào một ứng dụng. Cách đơn giản để đưa một mạch ESP32 tự làm vào Home Assistant là MQTT kèm chức năng discovery: ESP32 tự gửi tin mô tả mình có những công tắc, cảm biến nào, Home Assistant đọc và tạo thiết bị, không phải viết cấu hình YAML. Bài này dùng ESP32 và module relay 4 kênh: 4 kênh hiện thành 4 công tắc, kèm cảm biến cường độ Wi-Fi và trạng thái kết nối, nút nhấn tại chỗ vẫn dùng được và Home Assistant cập nhật theo.
1. ESP32, máy chủ MQTT và Home Assistant

MQTT hoạt động theo kiểu đăng và nhận tin theo chủ đề (topic) qua một máy chủ trung gian (broker). Cơ bản về MQTT và thư viện PubSubClient có trong bài dùng MQTT với ESP32. Trong bài này:
- Máy chủ MQTT là Mosquitto cài ngay trong Home Assistant.
- Khi kết nối, ESP32 gửi 5 tin cấu hình vào các chủ đề
homeassistant/…/config: 4 công tắc và 1 cảm biến. Home Assistant theo dõi các chủ đề này và tạo thiết bị. - Bấm công tắc trên Home Assistant: Home Assistant gửi
ONhoặcOFFvào chủ đề lệnh của kênh đó. ESP32 đổi relay rồi gửi lại trạng thái thật vào chủ đề trạng thái.
2. Chuẩn bị
- ESP32 DevKit V1 30 chân.
- Module relay 4 kênh 5 V kích mức thấp, một nút nhấn.
- Breadboard, dây cắm, dây USB nạp code.
- Một máy đang chạy Home Assistant trong cùng mạng Wi-Fi với ESP32. Nên đặt IP cố định cho máy này trên modem (gán IP theo địa chỉ MAC), vì chương trình ESP32 ghi IP của máy chủ MQTT.
3. Sơ đồ nối

Relay đóng cắt điện 220 V phải lắp trong hộp kín, dây lưới điện có cầu chì và đủ tiết diện; khi chưa quen làm điện, chỉ thử với đèn LED hoặc tải điện áp thấp.
4. Cài máy chủ MQTT và tài khoản trên Home Assistant
- Cài Mosquitto: trong Home Assistant vào Settings › Add-ons (bản mới đổi tên thành Apps), mở cửa hàng, tìm Mosquitto broker, bấm Install rồi Start. Mục này chỉ có ở bản Home Assistant OS; bản chạy bằng Docker thì cài Mosquitto riêng.
- Bật tích hợp MQTT: vào Settings › Devices & services. Home Assistant tự phát hiện MQTT; bấm thêm vào và xác nhận. Từ đây Home Assistant lắng nghe các tin cấu hình trong
homeassistant/…. - Tạo tài khoản cho thiết bị: Mosquitto trong Home Assistant không cho kết nối ẩn danh, nhưng nhận tài khoản người dùng của Home Assistant. Vào Settings › People, thêm một người dùng riêng cho các mạch ESP32 (bật cho phép đăng nhập, đặt tên đăng nhập và mật khẩu). Không dùng tài khoản quản trị của mình cho thiết bị.
5. Chương trình
Cần hai thư viện cài từ Library Manager của Arduino IDE: PubSubClient (Nick O'Leary) và ArduinoJson (Benoit Blanchon, bản 7). Trước khi nạp, sửa:
TEN_WIFI,MAT_KHAU_WIFI: Wi-Fi nhà (2,4 GHz).MQTT_MAY_CHU: IP của máy chạy Home Assistant.MQTT_TAI_KHOAN,MQTT_MAT_KHAU: tài khoản vừa tạo ở bước 4.
// PYWORLD.VN - Đưa ESP32 và relay 4 kênh vào Home Assistant bằng MQTT discovery
// Home Assistant tự tạo 4 công tắc và cảm biến cường độ Wi-Fi, không phải viết cấu hình YAML
// Thư viện: PubSubClient 2.8, ArduinoJson 7; máy chủ MQTT là Mosquitto chạy cùng Home Assistant
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
const char *TEN_WIFI = "TEN_WIFI";
const char *MAT_KHAU_WIFI = "MAT_KHAU_WIFI";
const char *MQTT_MAY_CHU = "192.168.1.20"; // Địa chỉ IP của máy chạy Home Assistant
const uint16_t MQTT_CONG = 1883;
const char *MQTT_TAI_KHOAN = "TAI_KHOAN_MQTT"; // Tài khoản người dùng Home Assistant dành cho thiết bị
const char *MQTT_MAT_KHAU = "MAT_KHAU_MQTT";
const uint8_t RELAY_PINS[4] = {32, 33, 26, 27}; // IN1, IN2, IN3, IN4
const bool RELAY_ACTIVE_LOW = true; // Module kích mức thấp: IN = LOW thì relay đóng
const uint8_t NUT = 4; // Nút nhấn nối GPIO4 xuống GND, đảo kênh 1
const unsigned long CHU_KY_RSSI_MS = 60000; // Gửi cường độ Wi-Fi mỗi phút
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
char maThietBi[20]; // pw_relay_ + 6 ký tự cuối của địa chỉ MAC, cũng là client ID của MQTT
bool relayBat[4] = {false, false, false, false};
bool henGuiCauHinh = false;
unsigned long lucHen = 0, treHen = 0;
// Mọi chủ đề của thiết bị nằm dưới pyworld/<mã thiết bị>/
String chuDe(const String &duoi) {
return String("pyworld/") + maThietBi + "/" + duoi;
}
void datRelay(uint8_t kenh, bool bat) {
relayBat[kenh] = bat;
bool muc = RELAY_ACTIVE_LOW ? !bat : bat;
digitalWrite(RELAY_PINS[kenh], muc ? HIGH : LOW);
Serial.printf("Relay K%u: %s\n", (unsigned)(kenh + 1), bat ? "BAT" : "TAT");
}
// Trạng thái gửi kèm cờ retain: Home Assistant khởi động lại vẫn biết relay đang bật hay tắt
void guiTrangThaiRelay(uint8_t kenh) {
mqtt.publish(chuDe("relay" + String(kenh + 1) + "/trang_thai").c_str(), relayBat[kenh] ? "ON" : "OFF", true);
}
void guiRssi() {
mqtt.publish(chuDe("rssi").c_str(), String(WiFi.RSSI()).c_str());
}
// Phần "device" giống nhau ở mọi thực thể: Home Assistant gom chúng vào một thiết bị
void themThietBi(JsonDocument &doc) {
doc["availability_topic"] = chuDe("ket_noi");
JsonObject dev = doc["device"].to<JsonObject>();
dev["identifiers"].add(maThietBi);
dev["name"] = "Relay PYWORLD";
dev["model"] = "ESP32 DevKit V1 + relay 4 kênh";
dev["sw_version"] = "1.0";
doc["origin"]["name"] = "pyworld.vn ESP32 MQTT";
}
bool guiCauHinhMot(const String &chuDeCauHinh, JsonDocument &doc) {
char buf[700];
size_t n = serializeJson(doc, buf, sizeof(buf));
if (n >= sizeof(buf) - 1) {
Serial.println("Cau hinh qua dai, tang kich thuoc buf");
return false;
}
return mqtt.publish(chuDeCauHinh.c_str(), (const uint8_t *)buf, n, false);
}
// Discovery: mỗi thực thể một tin cấu hình tại homeassistant/<loại>/<mã thiết bị>/<tên>/config
void guiCauHinh() {
uint8_t dat = 0;
for (uint8_t i = 0; i < 4; i++) {
JsonDocument doc;
String k = String(i + 1);
doc["name"] = "Kênh " + k;
doc["unique_id"] = String(maThietBi) + "_relay" + k;
doc["command_topic"] = chuDe("relay" + k + "/dat");
doc["state_topic"] = chuDe("relay" + k + "/trang_thai");
themThietBi(doc);
if (guiCauHinhMot(String("homeassistant/switch/") + maThietBi + "/relay" + k + "/config", doc)) dat++;
}
JsonDocument doc;
doc["name"] = "Cường độ Wi-Fi";
doc["unique_id"] = String(maThietBi) + "_rssi";
doc["state_topic"] = chuDe("rssi");
doc["device_class"] = "signal_strength";
doc["unit_of_measurement"] = "dBm";
doc["state_class"] = "measurement";
doc["entity_category"] = "diagnostic";
themThietBi(doc);
if (guiCauHinhMot(String("homeassistant/sensor/") + maThietBi + "/rssi/config", doc)) dat++;
Serial.printf("Da gui cau hinh discovery: %u/5 thuc the\n", (unsigned)dat);
for (uint8_t i = 0; i < 4; i++) guiTrangThaiRelay(i);
guiRssi();
}
void nhanTin(char *topic, byte *payload, unsigned int dai) {
String t(topic);
String noiDung;
for (unsigned int i = 0; i < dai; i++) noiDung += (char)payload[i];
Serial.printf("Nhan %s: %s\n", topic, noiDung.c_str());
// Home Assistant vừa khởi động (hoặc vừa nối lại máy chủ MQTT): gửi lại cấu hình sau một khoảng ngẫu nhiên
if (t == "homeassistant/status") {
if (noiDung == "online") {
henGuiCauHinh = true;
lucHen = millis();
treHen = random(500, 3000);
}
return;
}
for (uint8_t i = 0; i < 4; i++) {
if (t == chuDe("relay" + String(i + 1) + "/dat")) {
if (noiDung == "ON") datRelay(i, true);
else if (noiDung == "OFF") datRelay(i, false);
guiTrangThaiRelay(i); // Báo lại trạng thái thật để công tắc trên Home Assistant khớp với relay
}
}
}
void ketNoiMqtt() {
static unsigned long lanThu = 0;
static bool lanDau = true;
if (!lanDau && millis() - lanThu < 5000) return; // Lỗi thì 5 giây sau thử lại, không chặn vòng lặp lâu
lanDau = false;
lanThu = millis();
Serial.printf("Ket noi MQTT %s:%u...\n", MQTT_MAY_CHU, (unsigned)MQTT_CONG);
// Di chúc (last will): mất kết nối thì máy chủ MQTT tự đăng "offline", Home Assistant báo thiết bị không khả dụng
if (mqtt.connect(maThietBi, MQTT_TAI_KHOAN, MQTT_MAT_KHAU, chuDe("ket_noi").c_str(), 0, true, "offline")) {
Serial.printf("MQTT da ket noi, ma thiet bi %s\n", maThietBi);
mqtt.publish(chuDe("ket_noi").c_str(), "online", true);
mqtt.subscribe("homeassistant/status");
mqtt.subscribe(chuDe("+/dat").c_str());
guiCauHinh();
} else {
Serial.printf("MQTT loi %d, thu lai sau 5 giay\n", mqtt.state());
}
}
// Nút nhấn tại chỗ đảo trạng thái K1, vẫn dùng được khi mất mạng
void kiemTraNut() {
static bool docTruoc = HIGH, onDinh = HIGH;
static unsigned long lucDoi = 0;
bool doc = digitalRead(NUT);
if (doc != docTruoc) {
docTruoc = doc;
lucDoi = millis();
}
if (millis() - lucDoi > 30 && doc != onDinh) { // Chống dội 30 ms
onDinh = doc;
if (onDinh == LOW) {
datRelay(0, !relayBat[0]);
if (mqtt.connected()) guiTrangThaiRelay(0);
}
}
}
void setup() {
Serial.begin(115200);
for (uint8_t i = 0; i < 4; i++) {
pinMode(RELAY_PINS[i], INPUT); // Chân chưa xuất tín hiệu
digitalWrite(RELAY_PINS[i], RELAY_ACTIVE_LOW ? HIGH : LOW); // Ghi sẵn mức tắt
pinMode(RELAY_PINS[i], OUTPUT); // Bật ngõ ra, relay vẫn tắt
}
pinMode(NUT, INPUT_PULLUP);
WiFi.mode(WIFI_STA);
String mac = WiFi.macAddress(); // Dạng AA:BB:CC:DD:EE:FF
mac.replace(":", "");
mac.toLowerCase();
snprintf(maThietBi, sizeof(maThietBi), "pw_relay_%s", mac.substring(6).c_str());
WiFi.begin(TEN_WIFI, MAT_KHAU_WIFI);
mqtt.setServer(MQTT_MAY_CHU, MQTT_CONG);
mqtt.setBufferSize(1024); // Mặc định 256 byte, không đủ cho tin cấu hình discovery
mqtt.setCallback(nhanTin);
Serial.println("Dang ket noi Wi-Fi...");
}
void loop() {
kiemTraNut();
if (WiFi.status() != WL_CONNECTED) return;
static bool daBaoIp = false;
if (!daBaoIp) {
daBaoIp = true;
Serial.printf("Da vao Wi-Fi, IP %s\n", WiFi.localIP().toString().c_str());
}
if (!mqtt.connected()) {
ketNoiMqtt();
return;
}
mqtt.loop();
if (henGuiCauHinh && millis() - lucHen > treHen) {
henGuiCauHinh = false;
guiCauHinh();
}
static unsigned long lucGuiRssi = 0;
if (millis() - lucGuiRssi > CHU_KY_RSSI_MS) {
lucGuiRssi = millis();
guiRssi();
}
}
6. Giải thích các phần chính

Tin cấu hình discovery
Mỗi thực thể một tin, chủ đề dạng homeassistant/<loại>/<mã thiết bị>/<tên>/config. Loại switch là công tắc, sensor là cảm biến. Các khóa chính:
unique_id: mã riêng của thực thể, không được trùng và không được đổi; nhờ nó mới đổi tên, đổi biểu tượng thực thể trên Home Assistant được.device: cả 5 tin có cùngidentifiersnên Home Assistant gom vào một thiết bị tên "Relay PYWORLD".command_topic,state_topic: nơi nhận lệnh và nơi báo trạng thái. Lệnh mặc định làON/OFF.- Cảm biến có
device_class: signal_strengthvà đơn vị dBm;entity_category: diagnosticđưa nó vào nhóm chẩn đoán, không chen vào bảng điều khiển.
Mã thiết bị ghép từ 6 ký tự cuối địa chỉ MAC (pw_relay_…), nên nạp cùng chương trình cho nhiều mạch thì mỗi mạch là một thiết bị riêng. Tin cấu hình dài gần 400 byte, lớn hơn bộ đệm 256 byte mặc định của PubSubClient, vì vậy phải có mqtt.setBufferSize(1024).
Trạng thái kết nối và di chúc (last will)
Lúc kết nối, ESP32 gửi kèm "di chúc": chủ đề ket_noi, nội dung offline. Nếu ESP32 mất điện hoặc mất Wi-Fi, máy chủ MQTT tự đăng tin này, Home Assistant chuyển các thực thể sang không khả dụng thay vì hiện trạng thái cũ như thể vẫn đúng. Kết nối lại thì ESP32 đăng online.
Giữ lại trạng thái, gửi lại cấu hình khi Home Assistant khởi động
- Tin trạng thái relay và
ket_noigửi với cờ retain: máy chủ giữ tin cuối, Home Assistant khởi động lại là có ngay trạng thái. - Tin cấu hình gửi không có retain. Thay vào đó ESP32 theo dõi
homeassistant/status: mỗi lần Home Assistant khởi động, nó đăngonline, ESP32 chờ ngẫu nhiên 0,5–3 giây rồi gửi lại cấu hình. Tài liệu MQTT của Home Assistant khuyên cách này hơn tin cấu hình có retain, vì tin cấu hình bị giữ lại trên máy chủ dễ làm thực thể đã xóa tự hiện lại.
Sau khi mất điện
ESP32 khởi động với mọi relay tắt và báo OFF lên Home Assistant. Muốn relay về trạng thái cũ, có thể lưu trạng thái bằng Preferences như bài lưu dữ liệu khi mất điện, rồi gửi trạng thái đó lên khi kết nối.
7. Kết quả

Vài giây sau khi ESP32 kết nối, vào Settings › Devices & services › MQTT sẽ thấy thiết bị "Relay PYWORLD". Tên thực thể là tên thiết bị cộng tên kênh, ví dụ "Relay PYWORLD Kênh 1", mã thực thể thường là switch.relay_pyworld_kenh_1. Các công tắc dùng được ngay trong bảng điều khiển, trên ứng dụng điện thoại và trong tự động hóa (ví dụ bật Kênh 1 lúc mặt trời lặn, tắt lúc 23 giờ).

Muốn xem các tin đang đi qua máy chủ, trong trang cấu hình của tích hợp MQTT có mục nghe một chủ đề (Listen to a topic): gõ pyworld/# để xem mọi tin của các mạch trong bài.
8. Lưu ý
- Chỉ dùng trong mạng nhà: cổng 1883 không mã hóa. Không mở cổng này ra Internet trên modem; điều khiển từ xa thì dùng cách truy cập từ xa của Home Assistant.
- Xóa thiết bị: có thể xóa thiết bị MQTT trong trang thiết bị của Home Assistant, nhưng ESP32 còn chạy thì lần kết nối sau nó gửi lại cấu hình và thiết bị hiện lại. Muốn bỏ hẳn thì tháo mạch hoặc nạp chương trình khác.
- Đổi tên kênh: đổi ngay trên Home Assistant (nhờ
unique_id), không phải sửa chương trình. Đổiunique_idtrong code sẽ tạo thực thể mới. - Máy chủ MQTT khác: Home Assistant yêu cầu máy chủ hỗ trợ MQTT 5; Mosquitto 2 có hỗ trợ và vẫn nhận các mạch dùng MQTT 3.1.1 như PubSubClient.
9. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Serial báo "MQTT loi 4" hoặc "MQTT loi 5" | Sai tên đăng nhập, mật khẩu, hoặc tài khoản không được phép | Kiểm tra tài khoản ở bước 4; thử đăng nhập Home Assistant bằng tài khoản đó |
| Serial báo "MQTT loi -2" | Sai IP máy chủ, Mosquitto chưa chạy, khác mạng | Kiểm tra IP máy Home Assistant, trạng thái Mosquitto, ESP32 và máy chủ cùng mạng |
| Kết nối được nhưng Home Assistant không hiện thiết bị | Chưa thêm tích hợp MQTT, hoặc tin cấu hình không gửi được | Thêm tích hợp MQTT; nghe chủ đề homeassistant/# xem tin có tới không; giữ setBufferSize(1024) |
| Thực thể hiện "không khả dụng" | ESP32 mất kết nối, máy chủ đã đăng tin di chúc | Xem Serial Monitor; kiểm tra Wi-Fi tại chỗ đặt mạch |
| Bấm công tắc, relay không đổi | Sai chân, module chưa có nguồn 5 V, chủ đề lệnh khác | Xem Serial có dòng "Nhan …/dat" không; kiểm tra dây IN và VCC |
| Hai mạch giành nhau một thiết bị, liên tục mất kết nối | Hai mạch cùng client ID (đã sửa tay mã thiết bị giống nhau) | Giữ mã thiết bị theo địa chỉ MAC như chương trình |
10. Xem thêm
- Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient
- Gateway LoRa sang MQTT bằng ESP32
- Nạp chương trình cho ESP32 qua Wi-Fi (OTA)
- Hộp mica cho ESP32 DevKit 30 chân
- Các bài cùng loạt: đưa ESP32 vào Home Assistant bằng ESPHome (không viết code Arduino), cấu hình Wi-Fi bằng điện thoại, điều khiển relay bằng Blynk IoT.
Xem thêm