Pendahuluan — Kenapa Waktu Penting?
Di artikel MQTT (#7) dan broker Mosquitto (#16), payload sensor sudah berupa JSON — tapi sering tanpa penanda waktu. Fungsi millis() di ESP32 hanya menghitung milidetik sejak boot; setelah deep sleep (#11) atau reset, angka itu kembali ke nol.
Artikel Tier 2 ini menutup celah itu: sinkronkan wall-clock time lewat NTP (Network Time Protocol), set zona waktu Indonesia, lalu sisipkan timestamp ke setiap publish MQTT. Ini wajib sebelum Artikel #18 (Python → MySQL) dan #19 (InfluxDB + Grafana) — database histori butuh waktu yang konsisten dan bisa diurutkan.
Prasyarat: ESP32 sudah connect WiFi (#4) dan publish MQTT (#7). Broker Mosquitto pribadi (#16) disarankan (bukan broker publik). Familiar NVS (#12) membantu untuk deploy lapangan.
millis() vs Waktu NTP
| Sumber waktu | Contoh | Cocok untuk histori DB? |
|---|---|---|
millis() | 452301 ms sejak boot | ❌ Reset tiap deep sleep / power cycle |
time(nullptr) setelah NTP | Unix epoch 1782977400 | ✅ Sortable, universal |
| ISO 8601 lokal | 2026-07-02T14:30:00 | ✅ Mudah dibaca manusia & Python |
NTP membutuhkan internet atau server waktu di LAN (UDP port 123). Di lab rumah, pool.ntp.org atau id.pool.ntp.org biasanya cukup — asalkan ESP32 sudah online via WiFi.
Zona Waktu Indonesia
| Zona | UTC offset | gmtOffset_sec | Contoh kota |
|---|---|---|---|
| WIB | UTC+7 | 7 * 3600 | Jakarta, Bandung, Surabaya |
| WITA | UTC+8 | 8 * 3600 | Makassar, Bali |
| WIT | UTC+9 | 9 * 3600 | Jayapura, Papua |
Indonesia tidak pakai daylight saving — set daylightOffset_sec = 0. Ganti offset di sketch sesuai lokasi node sensor kamu.
Di Arduino IDE, setelah upload, baris seperti Waktu lokal: Wednesday, July 02 2026 14:30:00 di Serial Monitor membuktikan SNTP berhasil. Jika tahun masih 1970, berarti NTP belum sinkron — jangan publish dulu.
Yang Kamu Butuhkan
- ESP32 DevKit + sensor DHT22 (GPIO 4, pull-up 10kΩ) — sama tutorial DHT22
- WiFi 2.4 GHz dengan akses internet (untuk NTP)
- Broker MQTT — contoh sketch memakai Mosquitto #16 di
192.168.1.50 - Library: PubSubClient, DHT sensor library, ArduinoJson
- Header bawaan ESP32:
time.h(SNTP) — tidak perlu install tambahan
Estimasi biaya: Rp 0 (NTP publik gratis; tidak ada komponen baru jika sudah punya stack MQTT).
Arsitektur: WiFi → NTP → Sensor → MQTT
[ ESP32 ]
| 1) WiFi connect
| 2) configTime() → pool.ntp.org (UDP :123)
| 3) Baca DHT22
| 4) JSON + timestamp → MQTT publish
v
[ Mosquitto #16 ]
|
+-- Subscriber CLI / Python (#18 nanti) — INSERT dengan kolom waktu
+-- Home Assistant (#21) — entity dengan last_changed akurat
Topic (konsisten Seri 2): kodingindonesia/esp32/dht22/data
Payload contoh:
{"suhu":28.5,"kelembaban":65.2,"timestamp":"2026-07-02T14:30:00","unix":1782977400}
Fungsi Sinkronisasi NTP
Panggil setelah WiFi connected, sebelum publish:
#include "time.h"
const char* ntpServer = "id.pool.ntp.org"; // atau pool.ntp.org
const long gmtOffset_sec = 7 * 3600; // WIB — ganti 8/9 untuk WITA/WIT
const int daylightOffset_sec = 0;
bool sinkronisasiNTP(int maxRetry = 15) {
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
struct tm timeinfo;
for (int i = 0; i < maxRetry; i++) {
if (getLocalTime(&timeinfo)) {
return true;
}
delay(500);
}
return false;
}
bool ambilTimestampISO(char* buf, size_t len) {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
return false;
}
strftime(buf, len, "%Y-%m-%dT%H:%M:%S", &timeinfo);
return true;
}
Pro tip: Jangan panggil NTP sebelum WiFi
WL_CONNECTED— permintaan SNTP akan gagal diam-diam. Beri timeout (loop di atas) agar tidak hang selamanya.
Kode Lengkap: DHT22 + MQTT + Timestamp
Ganti SSID, password WiFi, kredensial MQTT, dan offset zona waktu:
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <ArduinoJson.h>
#include "time.h"
const char* ssid = "NamaWiFiKamu";
const char* password = "PasswordWiFiKamu";
const char* mqttHost = "192.168.1.50";
const int mqttPort = 1883;
const char* mqttUser = "kindo_esp32";
const char* mqttPass = "GANTI_PASSWORD_MQTT";
const char* topicData = "kodingindonesia/esp32/dht22/data";
const char* ntpServer = "id.pool.ntp.org";
const long gmtOffset_sec = 7 * 3600;
const int daylightOffset_sec = 0;
#define DHT_PIN 4
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);
WiFiClient espClient;
PubSubClient mqttClient(espClient);
unsigned long lastPublishMs = 0;
bool sinkronisasiNTP(int maxRetry = 15) {
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
struct tm timeinfo;
for (int i = 0; i < maxRetry; i++) {
if (getLocalTime(&timeinfo)) {
return true;
}
delay(500);
}
return false;
}
bool ambilTimestampISO(char* buf, size_t len) {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
return false;
}
strftime(buf, len, "%Y-%m-%dT%H:%M:%S", &timeinfo);
return true;
}
bool koneksiWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
unsigned long mulai = millis();
while (WiFi.status() != WL_CONNECTED && millis() - mulai < 15000) {
delay(500);
}
return WiFi.status() == WL_CONNECTED;
}
bool koneksiMQTT() {
mqttClient.setServer(mqttHost, mqttPort);
mqttClient.setBufferSize(512);
String clientId = "ESP32-NTP-" + String((uint32_t)ESP.getEfuseMac(), HEX);
uint8_t percobaan = 0;
while (!mqttClient.connected() && percobaan < 5) {
if (mqttClient.connect(clientId.c_str(), mqttUser, mqttPass)) {
return true;
}
Serial.print("MQTT gagal, rc=");
Serial.println(mqttClient.state());
percobaan++;
delay(2000);
}
return false;
}
bool publishSensorDenganWaktu() {
float rh = dht.readHumidity();
float suhu = dht.readTemperature();
if (isnan(rh) || isnan(suhu)) {
return false;
}
char isoBuf[32] = "";
if (!ambilTimestampISO(isoBuf, sizeof(isoBuf))) {
Serial.println("Waktu belum sinkron — publish tanpa timestamp akurat");
}
StaticJsonDocument<192> doc;
doc["suhu"] = roundf(suhu * 10) / 10.0;
doc["kelembaban"] = roundf(rh * 10) / 10.0;
if (isoBuf[0] != '\0') {
doc["timestamp"] = isoBuf;
doc["unix"] = (long)time(nullptr);
}
char buffer[192];
serializeJson(doc, buffer);
mqttClient.loop();
return mqttClient.publish(topicData, buffer);
}
void setup() {
Serial.begin(115200);
dht.begin();
delay(2000);
if (!koneksiWiFi()) {
Serial.println("WiFi gagal");
return;
}
Serial.println("WiFi OK");
if (!sinkronisasiNTP()) {
Serial.println("NTP gagal — cek internet / firewall UDP 123");
} else {
struct tm timeinfo;
getLocalTime(&timeinfo);
Serial.println(&timeinfo, "Waktu lokal: %A, %B %d %Y %H:%M:%S");
}
if (!koneksiMQTT()) {
Serial.println("MQTT gagal");
return;
}
Serial.println("MQTT OK");
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
koneksiWiFi();
}
if (!mqttClient.connected()) {
koneksiMQTT();
}
mqttClient.loop();
if (millis() - lastPublishMs > 10000) {
lastPublishMs = millis();
if (publishSensorDenganWaktu()) {
Serial.println("Publish dengan timestamp OK");
}
}
}
Penjelasan Bagian Kritis
- Urutan: WiFi → NTP → baca sensor → publish. NTP butuh route ke internet.
configTime()— memanggil SNTP di balik layar ESP32 Arduino core.timestamp+unix— ISO lokal (tanpa suffix+07:00) untuk dashboard manusia; Unix UTC untuk query SQL/Grafana di #18/#19.- Max 5 percobaan MQTT —
koneksiMQTT()tidak blockloop()selamanya jika broker down (pola sama #17). setBufferSize(512)— JSON lebih panjang setelah tambah field waktu.- Broker #16 — ganti ke port
8883+ TLS jika sudah ikut artikel #17. - Deep sleep (#11): sinkronkan NTP tiap bangun — RTC tidak menyimpan zona waktu setelah reset penuh.
Integrasi Deep Sleep & Node Lapangan
Pada node deep sleep (#11), tambahkan sinkronisasiNTP() di setup() setelah WiFi — sebelum publishSensorDenganWaktu(). Budget waktu: NTP biasanya 1–3 detik; tetap jauh lebih hemat daripada node always-on.
Untuk node tanpa internet (hanya LAN), pertimbangkan NTP server lokal di Raspberry Pi (ntp package) dan arahkan ntpServer ke IP Pi — di luar scope artikel ini, tapi pola configTime() sama.
Verifikasi dari Laptop (mosquitto_sub)
Setelah ESP32 publish, pantau payload lengkap dari terminal:
mosquitto_sub -h 192.168.1.50 -p 1883 \
-u kindo_esp32 -P 'GANTI_PASSWORD_MQTT' \
-t "kodingindonesia/esp32/dht22/data" -v
Output yang diharapkan:
kodingindonesia/esp32/dht22/data {"suhu":28.5,"kelembaban":65.2,"timestamp":"2026-07-02T14:30:00","unix":1782977400}
Field unix akan dipakai di Artikel #18 untuk kolom DATETIME atau INT UNSIGNED di MySQL — lebih mudah di-query daripada string ISO saja.
Di Grafana (#19), sumbu waktu grafik otomatis benar jika setiap titik data punya timestamp UTC atau lokal yang konsisten — tanpa NTP, grafik akan terlihat “loncat” atau semua titik menumpuk di waktu upload subscriber, bukan waktu pengukuran sensor di lapangan.
Uji Coba (Checklist)
- Upload sketch → Serial:
WiFi OKlalu baris waktu lokal terformat - Jika NTP gagal — cek router block UDP 123 atau DNS
mosquitto_sub -h 192.168.1.50 -p 1883 -u kindo_esp32 -P 'GANTI_PASSWORD_MQTT' -t "kodingindonesia/esp32/dht22/data" -v- Pastikan JSON punya
timestampdanunix— bukan hanya suhu/kelembaban - Ubah
gmtOffset_secke WITA/WIT — bandingkan jam di payload dengan jam HP (zona sama) - Reset board —
unixharus tetap masuk akal (bukan 0 atau 1970) - Gabung dengan PIR (#24) — tambahkan
timestampke event gerak
Tips & Troubleshooting
- NTP selalu gagal: WiFi belum connect; firewall blok UDP 123; coba
pool.ntp.orgatau IP NTP router - Jam salah ± beberapa jam:
gmtOffset_secsalah — sesuaikan WIB/WITA/WIT timestampkosong di JSON:getLocalTime()gagal — perpanjang retry atau cek SNTP setelah WiFi stabil- Publish gagal setelah tambah field: Naikkan
setBufferSize(512)atau kurangi presisi string - Deep sleep — waktu 1970: Normal jika lupa panggil NTP setelah bangun; panggil ulang di awal
setup() - WiFi 2.4 GHz: ESP32 tidak support jaringan 5 GHz saja
- rc=5 MQTT: Sama seperti #16 — user/password salah
Langkah Selanjutnya (Seri 2)
- Artikel #18: Subscriber Python — parse
timestamp/unix→ simpan ke MySQL (kolomDATETIME) - Artikel #19: InfluxDB + Grafana — histori grafik dengan sumbu waktu benar
- Home Assistant (#21) — sensor MQTT dengan timestamp di
last_changed - MQTT TLS (#17) — amankan transport sebelum subscriber cloud
- Deep sleep (#11) — gabung NTP tiap siklus bangun
- NVS (#12) — simpan offset zona / host NTP opsional
Keamanan: Jangan hardcode password WiFi atau MQTT di repo publik. Gunakan NVS + WiFiManager (#12) untuk deploy lapangan.
Dengan timestamp akurat, data sensor siap masuk pipeline histori — langkah berikutnya adalah Artikel #18 Python → MySQL. Lanjutkan Seri 2 di halaman artikel Koding Indonesia.