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 waktuContohCocok untuk histori DB?
millis()452301 ms sejak boot❌ Reset tiap deep sleep / power cycle
time(nullptr) setelah NTPUnix epoch 1782977400✅ Sortable, universal
ISO 8601 lokal2026-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

ZonaUTC offsetgmtOffset_secContoh kota
WIBUTC+77 * 3600Jakarta, Bandung, Surabaya
WITAUTC+88 * 3600Makassar, Bali
WITUTC+99 * 3600Jayapura, 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

  1. Urutan: WiFi → NTP → baca sensor → publish. NTP butuh route ke internet.
  2. configTime() — memanggil SNTP di balik layar ESP32 Arduino core.
  3. timestamp + unix — ISO lokal (tanpa suffix +07:00) untuk dashboard manusia; Unix UTC untuk query SQL/Grafana di #18/#19.
  4. Max 5 percobaan MQTTkoneksiMQTT() tidak block loop() selamanya jika broker down (pola sama #17).
  5. setBufferSize(512) — JSON lebih panjang setelah tambah field waktu.
  6. Broker #16 — ganti ke port 8883 + TLS jika sudah ikut artikel #17.
  7. 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)

  1. Upload sketch → Serial: WiFi OK lalu baris waktu lokal terformat
  2. Jika NTP gagal — cek router block UDP 123 atau DNS
  3. mosquitto_sub -h 192.168.1.50 -p 1883 -u kindo_esp32 -P 'GANTI_PASSWORD_MQTT' -t "kodingindonesia/esp32/dht22/data" -v
  4. Pastikan JSON punya timestamp dan unix — bukan hanya suhu/kelembaban
  5. Ubah gmtOffset_sec ke WITA/WIT — bandingkan jam di payload dengan jam HP (zona sama)
  6. Reset board — unix harus tetap masuk akal (bukan 0 atau 1970)
  7. Gabung dengan PIR (#24) — tambahkan timestamp ke event gerak

Tips & Troubleshooting

  • NTP selalu gagal: WiFi belum connect; firewall blok UDP 123; coba pool.ntp.org atau IP NTP router
  • Jam salah ± beberapa jam: gmtOffset_sec salah — sesuaikan WIB/WITA/WIT
  • timestamp kosong 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 (kolom DATETIME)
  • 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.