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
- WeMos D1 mini (ESP8266): WeMos D1 Mini ESP8266 – V2.0 nebo s externí anténou: WeMos D1 Mini Pro
- Senzorový shield teplota/vlhkost (I2C): SHT30 WeMos D1 mini Shield
- Micro USB kabel + USB nabíječka 5 V
- Volitelně později: LED matice 8×8 pro D1 mini, Relé shield
Schéma a montáž
- Nasazení shieldu: SHT30 shield nacvakni přímo na WeMos D1 mini (sedí pin-to-pin).
- Zapojení I2C: D1=SCL, D2=SDA (řeší shield, není třeba kabelovat).
- 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í
- V Arduino IDE vyberte desku LOLIN (WEMOS) D1 R2 & mini (ESP8266).
- Do kódu doplňte
WIFI_SSID/WIFI_PASSa nahrajte přes USB. - Po připojení k Wi-Fi otevřete
http://byt-teplomer.local/(nebo IP z routeru). - Další aktualizace dělejte už pohodlně přes OTA (Arduino IDE → Network ports).
API a správa
GET /– jednoduchá HTML stránka s hodnotamiGET /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 LittleFSGET /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
- WeMos D1 mini: D1 mini / D1 mini Pro
- SHT30 shield: SHT30 pro D1 mini
- LED 8×8 shield: LED matice 8×8
- Relé shield: Relé pro D1 mini
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.
FAQ - Časté dotazy:
Jaké komponenty potřebuji pro projekt Wi-Fi teploměru a vlhkoměru?
Jaký je postup pro nahrání kódu do WeMos D1 mini?
Jak funguje ukládání dat do JSON v projektu?
Jaké jsou možné problémy a jejich řešení při realizaci projektu?
Jak zajistit bezpečnost při práci s projektem?
Související produkty




