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.
Související produkty




