DIY: Wi-Fi teploměr & vlhkoměr (WeMos D1 mini + SHT30) s webem, OTA a ukládáním do JSON (LittleFS/SPIFFS)

Jednoduchý a bezpečný projekt pro začátečníky na WeMos D1 mini (ESP8266) a SHT30 senzoru. Běží na 5 V (USB), zveřejňuje hodnoty na webové stránce, má OTA aktualizace (bezdrátové nahrávání nového FW) a ukládá data i konfiguraci do JSON v LittleFS (lze přepnout na SPIFFS). Volitelně ho později rozšíříš o relé nebo LED matici.

Co budete potřebovat

Schéma a montáž

  1. Nasazení shieldu: SHT30 shield nacvakni přímo na WeMos D1 mini (sedí pin-to-pin).
  2. Zapojení I2C: D1=SCL, D2=SDA (řeší shield, není třeba kabelovat).
  3. Napájení: 5 V přes micro USB, místnost bez přímého slunce/topidla (~1,2–1,5 m).

Arduino verze s OTA a JSON v LittleFS

Níže je hotový kód pro Arduino IDE (ESP8266 jádro). Umí web (/, /json), OTA, mDNS, konfiguraci přes /config (GET/POST) a ukládá stav do /state.json. Výchozí úložiště je LittleFS, jedním přepínačem lze použít SPIFFS.

Knihovny

  • ESP8266 core (Board Manager)
  • ESP8266WiFi, ESP8266WebServer, ArduinoOTA, ESP8266mDNS (součást core)
  • LittleFS (součást core; volitelně SPIFFS)
  • Adafruit SHT31 (Library Manager)
  • ArduinoJson 6.x (Library Manager)

Kód

/* 
 * Wi-Fi teploměr & vlhkoměr (ESP8266 D1 mini + SHT30)
 * - Web UI (/, /json)
 * - OTA (ArduinoOTA), mDNS (http://byt-teplomer.local)
 * - JSON persist: /config.json a /state.json v LittleFS (lze přepnout na SPIFFS)
 * - I2C: SDA=D2, SCL=D1
 */

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <ArduinoOTA.h>
#include <Wire.h>
#include "Adafruit_SHT31.h"

#include <ArduinoJson.h>

// ---- FS: LittleFS (lze přepnout na SPIFFS) ----
#include <LittleFS.h>
#define FSYS LittleFS
// Pro SPIFFS: 
// #include <FS.h>
// #define FSYS SPIFFS

// ====== UPRAVTE PODLE SEBE ======
const char* WIFI_SSID = "NAZEV_WIFI";
const char* WIFI_PASS = "TAJNE_HESLO";
const char* MDNS_NAME = "byt-teplomer"; // http://byt-teplomer.local
// =================================

ESP8266WebServer server(80);
Adafruit_SHT31 sht = Adafruit_SHT31();

struct AppConfig { uint16_t measure_interval_sec = 15; } CONFIG;
struct AppState  { float last_temp = NAN; float last_humi = NAN; uint32_t samples = 0; uint32_t boot_time = 0; } STATE;

const char* CFG_PATH   = "/config.json";
const char* STATE_PATH = "/state.json";

bool loadConfig() {
  if (!FSYS.exists(CFG_PATH)) return false;
  File f = FSYS.open(CFG_PATH, "r"); if (!f) return false;
  StaticJsonDocument<256> doc; auto err = deserializeJson(doc, f); f.close(); if (err) return false;
  CONFIG.measure_interval_sec = doc["measure_interval_sec"] | CONFIG.measure_interval_sec;
  return true;
}

bool saveConfig() {
  StaticJsonDocument<256> doc;
  doc["measure_interval_sec"] = CONFIG.measure_interval_sec;
  File f = FSYS.open(CFG_PATH, "w"); if (!f) return false;
  serializeJson(doc, f); f.close(); return true;
}

bool saveState() {
  StaticJsonDocument<256> doc;
  doc["last_temp"] = isnan(STATE.last_temp) ? nullptr : STATE.last_temp;
  doc["last_humi"] = isnan(STATE.last_humi) ? nullptr : STATE.last_humi;
  doc["samples"]   = STATE.samples;
  doc["uptime_s"]  = (millis() - STATE.boot_time) / 1000;
  File f = FSYS.open(STATE_PATH, "w"); if (!f) return false;
  serializeJson(doc, f); f.close(); return true;
}

String htmlIndex() {
  String h = F("<!doctype html><html><head><meta charset='utf-8'/>"
               "<meta name='viewport' content='width=device-width,initial-scale=1'/>"
               "<title>Byt Teploměr</title>"
               "<style>body{font:16px/1.5 system-ui,Arial;margin:20px;} .card{max-width:520px;padding:16px;border:1px solid #ddd;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.06);} h1{font-size:20px;margin:0 0 12px;} .kv{display:flex;gap:16px} .kv div{flex:1;background:#f8f8f8;padding:12px;border-radius:10px;text-align:center} .small{color:#666;font-size:13px;margin-top:10px}</style>"
               "</head><body><div class='card'><h1>Byt Teploměr & Vlhkoměr</h1><div class='kv'>");
  h += "<div><div>Teplota</div><div style='font-size:28px'>"; h += isnan(STATE.last_temp) ? "-" : String(STATE.last_temp,1) + " °C"; h += "</div></div>";
  h += "<div><div>Vlhkost</div><div style='font-size:28px'>"; h += isnan(STATE.last_humi) ? "-" : String(STATE.last_humi,1) + " %RH"; h += "</div></div>";
  h += F("</div><div class='small'>Vzorky: "); h += String(STATE.samples);
  h += F(" • Interval: "); h += String(CONFIG.measure_interval_sec);
  h += F(" s • <a href='/json'>/json</a> • <a href='/config'>/config</a></div></div></body></html>");
  return h;
}

void handleRoot(){ server.send(200, "text/html; charset=utf-8", htmlIndex()); }

void handleJson(){
  StaticJsonDocument<256> doc;
  doc["temperature"] = isnan(STATE.last_temp) ? nullptr : STATE.last_temp;
  doc["humidity"]    = isnan(STATE.last_humi) ? nullptr : STATE.last_humi;
  doc["samples"]     = STATE.samples;
  doc["interval_s"]  = CONFIG.measure_interval_sec;
  doc["uptime_s"]    = (millis() - STATE.boot_time) / 1000;
  String out; serializeJson(doc, out);
  server.sendHeader("Access-Control-Allow-Origin", "*");
  server.send(200, "application/json", out);
}

void handleConfigGet(){
  StaticJsonDocument<128> doc;
  doc["measure_interval_sec"] = CONFIG.measure_interval_sec;
  String out; serializeJsonPretty(doc, out);
  server.sendHeader("Access-Control-Allow-Origin", "*");
  server.send(200, "application/json", out);
}

void handleConfigPost(){
  if (!server.hasArg("plain")) { server.send(400,"application/json","{\"error\":\"missing body\"}"); return; }
  StaticJsonDocument<256> doc;
  auto err = deserializeJson(doc, server.arg("plain")); if (err){ server.send(400,"application/json","{\"error\":\"invalid json\"}"); return; }
  if (doc.containsKey("measure_interval_sec")){
    int v = doc["measure_interval_sec"].as<int>();
    if (v < 2) v = 2; if (v > 3600) v = 3600;
    CONFIG.measure_interval_sec = (uint16_t)v;
  }
  if (!saveConfig()){ server.send(500,"application/json","{\"error\":\"failed to save config\"}"); return; }
  handleConfigGet();
}

void handleStateFile(){
  if (!FSYS.exists("/state.json")){ server.send(404,"application/json","{\"error\":\"state.json not found\"}"); return; }
  File f = FSYS.open("/state.json","r"); server.streamFile(f,"application/json"); f.close();
}

void handleNotFound(){ server.send(404, "text/plain", "Not found"); }

void setupWiFi(){
  WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS);
  uint32_t t=millis(); while (WiFi.status()!=WL_CONNECTED && millis()-t<20000) delay(250);
}

void setupOTA(){
  ArduinoOTA.setHostname(MDNS_NAME);
  ArduinoOTA.begin();
}

void setupMDNS(){
  if (MDNS.begin(MDNS_NAME)) MDNS.addService("http","tcp",80);
}

uint32_t lastMeasure=0;
void doMeasure(){
  float t = sht.readTemperature();
  float h = sht.readHumidity();
  if (!isnan(t)) STATE.last_temp = t;
  if (!isnan(h)) STATE.last_humi = h;
  STATE.samples++;
  saveState();
}

void setup(){
  Serial.begin(115200); delay(100);
  FSYS.begin(); loadConfig(); STATE.boot_time = millis();
  Wire.begin(D2, D1); sht.begin(0x44); delay(50);
  setupWiFi(); setupMDNS(); setupOTA();

  server.on("/", HTTP_GET, handleRoot);
  server.on("/json", HTTP_GET, handleJson);
  server.on("/config", HTTP_GET, handleConfigGet);
  server.on("/config", HTTP_POST, handleConfigPost);
  server.on("/state.json", HTTP_GET, handleStateFile);
  server.onNotFound(handleNotFound);
  server.begin();

  doMeasure();
}

void loop(){
  ArduinoOTA.handle();
  server.handleClient();
  if (millis()-lastMeasure >= (uint32_t)CONFIG.measure_interval_sec*1000UL){
    lastMeasure = millis();
    doMeasure();
  }
}

Nahrání a první spuštění

  1. V Arduino IDE vyberte desku LOLIN (WEMOS) D1 R2 & mini (ESP8266).
  2. Do kódu doplňte WIFI_SSID/WIFI_PASS a nahrajte přes USB.
  3. Po připojení k Wi-Fi otevřete http://byt-teplomer.local/ (nebo IP z routeru).
  4. Další aktualizace dělejte už pohodlně přes OTA (Arduino IDE → Network ports).

API a správa

  • GET / – jednoduchá HTML stránka s hodnotami
  • GET /json – aktuální stav: {"temperature":24.3,"humidity":45.2,...}
  • GET /config – konfigurace (JSON), aktuálně {"measure_interval_sec":15}
  • POST /config – uloží JSON (např. {"measure_interval_sec":30}), přetrvá v LittleFS
  • GET /state.json – uložený stav přímo ze souboru

LittleFS ↔ SPIFFS

Pokud preferuješ SPIFFS, nahoře v kódu nahraď hlavičky a #define FSYS dle komentářů a použij SPIFFS.begin(). Struktura souborů a endpointy zůstávají stejné.

Tipy & odstraňování potíží

  • Nezobrazuje hodnoty: zkontroluj I2C (D2/D1), adresu 0x44, knihovny a napájení 5 V.
  • OTA/MDNS nefunguje: stejné VLANy, povolený mDNS ve Windows/OS, případně použij přímo IP.
  • „Skákající“ hodnoty: zvětši interval měření (20–30 s) a umísti modul mimo průvan a zdroj tepla.

Bezpečnost

Projekt je slaboproudý (5 V). Pokud později přidáš relé na 230 V, silovou část musí zapojit kvalifikovaný elektrikář.

Rychlé odkazy na součástky


Hotovo! Máte hotový Wi-Fi snímač klimatu do bytu s OTA a uložením dat. Snadno rozšiřitelný o zobrazení na LED matici, automatizace a další senzory.

Související produkty

Tento web slouží k prezentaci a propagaci produktů našich partnerů a nelze zde objednávat.
Kliknutím na vybrat velikost přejdete do e-shopu prodejce, kde si můžete výrobky objednat. - Podmínky užití webu

REKLAMA

Pneuservis a opravy pneu Praha Nehvizdy