Самодельная метеостанция для дачи на ESP32: полное руководство

Помните это чувство? Вы просыпаетесь ранним утром на даче, смотрите в окно — и думаете: «А не замёрз ли картофель в подвале?» Или: «Не перегрелся ли котёл, пока я спал?» Раньше я просто бегал с термометром по дому. А потом собрал умную метеостанцию на ESP32 — и теперь всё, что мне нужно — просто спросить:

«Алиса, какая температура в подвале?»
«В подвале 12.3 градуса» — отвечает она, как настоящий дворецкий.

И это не фантастика — это ваша собственная система, собранная за выходные!

Почему именно самодельная?
Готовые метеостанции стоят дорого, не всегда показывают давление, редко умеют работать с умным домом. А моя —
✅ Показывает всё: температуру в доме, на улице, у котла, в подвале
✅ Рисует графики — вы видите, как давление падает перед дождём
✅ Говорит с Алисой — никаких приложений, просто голос

внешний вид устройства уже установленного на даче

Что внутри?

Я взял:

  • ESP32 Dev Module — мозг системы (Wi-Fi + Bluetooth «из коробки»)
  • TFT-дисплей 1.8″ ST7735 (128×160) — чтобы видеть всё без телефона
  • BME280 — точный датчик температуры, влажности и атмосферного давления
  • Три DS18B20 — один на улицу, один к котлу, один в подвал
  • Сенсорную кнопку TTP223 — экран включается касанием и гаснет через 20 секунд (экономия энергии!)
  • Макетная плата и провода
  • Конденсатор 100 мкФ для стабилизации питания

Как это работает?

Утром вы подходите к станции, касаетесь пальцем — экран загорается, и вы видите:

1 Temp 23.5°C

2 Vlag 45 %

3 Dav 1013 hPa

4 Ulica 18.2°C

5 Batareya 28.7°C

6 Ulica12.0°C

А если лень вставать — просто спрашиваете у Алисы.
А если хотите проанализировать — заходите в Zabbix или Home Assistant, где живые графики показывают, как менялась температура за неделю.

Схема подключения

TFT VCC3.3V
TFT GNDGND
TFT LEDGPIO26
TFT SCKGPIO18
TFT MOSIGPIO23
TFT CSGPIO5
TFT DCGPIO16
TFT RSTGPIO17
BME280 SDAGPIO21
BME280 SCLGPIO22
DS18B20 (все три) DATAGPIO4 + резистор 4.7 кОм к 3.3V
TTP223 OUTGPIO14

Программная часть

Устанавливаем Visual Studio Code с расширением PlatformIO. В файле platformio.ini указываем зависимости:

lib_deps =
adafruit/Adafruit ST7735 and ST7789 Library
adafruit/Adafruit GFX Library
adafruit/Adafruit BME280 Library
adafruit/Adafruit Unified Sensor
paulstoffregen/OneWire
milesburton/DallasTemperature
bblanchon/ArduinoJson

Полный скетч включает:

  • Отображение данных на TFT в книжной ориентации
  • Управление подсветкой: по умолчанию выключена, включается на 20 секунд по сенсорной кнопке
  • Веб-интерфейс с возможностью переименования датчиков
  • JSON-выход для интеграции с Home Assistant и Zabbix
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <Adafruit_BME280.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <ArduinoJson.h>
#include "nvs.h"
#include "nvs_flash.h"

// === Wi-Fi ===
const char* ssid = "SSID";
const char* password = "PASSWORD";

// === Пины ===
#define TFT_CS     5
#define TFT_DC    16
#define TFT_RST   17
#define LED_PIN   26   // подсветка TFT
#define TOUCH_PIN 14   // TTP223 OUT → GPIO14
#define DS18B20_PIN 4

// === Прототипы функций ===
void drawScreen();
void readSensors();
void handleRoot();
void handleData();
void handleSettings();
void handleSave();
void loadNames();
void saveNames();

// === Объекты ===
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
Adafruit_BME280 bme;
OneWire oneWire(DS18B20_PIN);
DallasTemperature sensors(&oneWire);
WebServer server(80);

// === Данные ===
float temperature = NAN;
float humidity = NAN;
float pressure = NAN;
float tempUlica = NAN;
float tempBatareya = NAN;
float tempRezerv = NAN;

String name1 = "Ulica";
String name2 = "Batareya";
String name3 = "Rezerv";

bool displayOn = false;
unsigned long screenOnUntil = 0;

// === Чтение датчиков ===
void readSensors() {
  temperature = bme.readTemperature();
  humidity = bme.readHumidity();
  pressure = bme.readPressure() / 100.0;

  sensors.requestTemperatures();
  int deviceCount = sensors.getDeviceCount();

  if (deviceCount >= 1) {
    tempUlica = sensors.getTempCByIndex(0);
    if (tempUlica == -127) tempUlica = NAN;
  } else { tempUlica = NAN; }

  if (deviceCount >= 2) {
    tempBatareya = sensors.getTempCByIndex(1);
    if (tempBatareya == -127) tempBatareya = NAN;
  } else { tempBatareya = NAN; }

  if (deviceCount >= 3) {
    tempRezerv = sensors.getTempCByIndex(2);
    if (tempRezerv == -127) tempRezerv = NAN;
  } else { tempRezerv = NAN; }
}

// === NVS: сохранение имён ===
void saveNames() {
  nvs_handle_t my_handle;
  esp_err_t err = nvs_open("storage", NVS_READWRITE, &my_handle);
  if (err == ESP_OK) {
    nvs_set_str(my_handle, "name1", name1.c_str());
    nvs_set_str(my_handle, "name2", name2.c_str());
    nvs_set_str(my_handle, "name3", name3.c_str());
    nvs_commit(my_handle);
    nvs_close(my_handle);
  }
}

void loadNames() {
  nvs_handle_t my_handle;
  esp_err_t err = nvs_open("storage", NVS_READWRITE, &my_handle);
  if (err == ESP_OK) {
    size_t len1 = 20, len2 = 20, len3 = 20;
    char buf1[20], buf2[20], buf3[20];
    if (nvs_get_str(my_handle, "name1", buf1, &len1) == ESP_OK) name1 = String(buf1);
    if (nvs_get_str(my_handle, "name2", buf2, &len2) == ESP_OK) name2 = String(buf2);
    if (nvs_get_str(my_handle, "name3", buf3, &len3) == ESP_OK) name3 = String(buf3);
    nvs_close(my_handle);
  }
}

// === Экран ===
void drawScreen() {
  if (!displayOn) {
    digitalWrite(LED_PIN, LOW);
    tft.fillScreen(ST7735_BLACK);
    return;
  }

  digitalWrite(LED_PIN, HIGH);
  tft.fillScreen(ST7735_BLACK);

  tft.setTextSize(1);
  tft.setTextColor(ST7735_YELLOW);
  tft.setCursor(0, 0);
  tft.print("METEO STATION");

  tft.setCursor(100, 0);
  tft.setTextColor(WiFi.status() == WL_CONNECTED ? ST7735_GREEN : ST7735_RED);
  tft.print(WiFi.status() == WL_CONNECTED ? "OK" : "--");

  int y = 20;
  const int labelX = 0;
  const int valueX = 80;

  tft.setTextSize(1);
  tft.setTextColor(ST7735_CYAN);
  tft.setCursor(labelX, y);
  tft.print("Temp");
  tft.setTextSize(2);
  tft.setCursor(valueX, y);
  if (!isnan(temperature)) tft.print(temperature, 1); else tft.print("--.-");
  y += 25;

  tft.setTextSize(1);
  tft.setTextColor(ST7735_GREEN);
  tft.setCursor(labelX, y);
  tft.print("Vlag");
  tft.setTextSize(2);
  tft.setCursor(valueX, y);
  if (!isnan(humidity)) tft.print(humidity, 0); else tft.print("--");
  y += 25;

  tft.setTextSize(1);
  tft.setTextColor(ST7735_MAGENTA);
  tft.setCursor(labelX, y);
  tft.print("Dav");
  tft.setTextSize(2);
  tft.setCursor(valueX, y);
  if (!isnan(pressure)) tft.print(pressure, 0); else tft.print("----");
  y += 25;

  tft.setTextSize(1);
  tft.setTextColor(ST7735_ORANGE);
  tft.setCursor(labelX, y);
  tft.print(name1);
  tft.setTextSize(2);
  tft.setCursor(valueX, y);
  if (!isnan(tempUlica)) tft.print(tempUlica, 1); else tft.print("--.-");
  y += 25;

  tft.setTextSize(1);
  tft.setTextColor(ST7735_RED);
  tft.setCursor(labelX, y);
  tft.print(name2);
  tft.setTextSize(2);
  tft.setCursor(valueX, y);
  if (!isnan(tempBatareya)) tft.print(tempBatareya, 1); else tft.print("--.-");
  y += 25;

  tft.setTextSize(1);
  tft.setTextColor(ST7735_WHITE);
  tft.setCursor(labelX, y);
  tft.print(name3);
  tft.setTextSize(2);
  tft.setCursor(valueX, y);
  if (!isnan(tempRezerv)) tft.print(tempRezerv, 1); else tft.print("--.-");
}

// === Веб-интерфейс ===
void handleRoot() {
  String html = "<html><head><meta charset='utf-8'>";
  html += "<meta http-equiv='refresh' content='5'>";
  html += "<title>Meteo Station</title></head><body>";
  html += "<h2>Метеостанция</h2>";
  html += "<p><b>Temp:</b> " + String(isnan(temperature) ? "--.-" : String(temperature, 1)) + "</p>";
  html += "<p><b>Vlag:</b> " + String(isnan(humidity) ? "--" : String(humidity, 0)) + "</p>";
  html += "<p><b>Dav:</b> " + String(isnan(pressure) ? "----" : String(pressure, 0)) + "</p>";
  html += "<p><b>" + name1 + ":</b> " + String(isnan(tempUlica) ? "--.-" : String(tempUlica, 1)) + "</p>";
  html += "<p><b>" + name2 + ":</b> " + String(isnan(tempBatareya) ? "--.-" : String(tempBatareya, 1)) + "</p>";
  html += "<p><b>" + name3 + ":</b> " + String(isnan(tempRezerv) ? "--.-" : String(tempRezerv, 1)) + "</p>";
  html += "<p><a href='/settings'>Изменить имена датчиков</a></p>";
  html += "<p><a href='/data'>JSON данные</a></p>";
  html += "</body></html>";
  server.send(200, "text/html", html);
}

void handleData() {
  readSensors();
  JsonDocument doc;
  doc["temperature"] = temperature;
  doc["humidity"] = humidity;
  doc["pressure"] = pressure;
  doc["temp1"] = tempUlica;
  doc["temp2"] = tempBatareya;
  doc["temp3"] = tempRezerv;
  doc["wifi"] = (WiFi.status() == WL_CONNECTED) ? "OK" : "OFF";
  String json;
  serializeJson(doc, json);
  server.send(200, "application/json", json);
}

void handleSettings() {
  String html = "<html><head><meta charset='utf-8'><title>Настройки</title></head><body>";
  html += "<h2>Имена датчиков</h2>";
  html += "<form method='POST' action='/save'>";
  html += "Датчик 1: <input type='text' name='n1' value='" + name1 + "'><br><br>";
  html += "Датчик 2: <input type='text' name='n2' value='" + name2 + "'><br><br>";
  html += "Датчик 3: <input type='text' name='n3' value='" + name3 + "'><br><br>";
  html += "<input type='submit' value='Сохранить'>";
  html += "</form>";
  html += "<p><a href='/'>Назад</a></p>";
  html += "</body></html>";
  server.send(200, "text/html", html);
}

// === Веб: сохранение (с обновлением экрана!) ===
void handleSave() {
  if (server.method() == HTTP_POST) {
    name1 = server.arg("n1");
    name2 = server.arg("n2");
    name3 = server.arg("n3");
    saveNames();
    
    // Обновляем экран, если он включён
    if (displayOn) {
      drawScreen();
    }
  }
  server.sendHeader("Location", "/");
  server.send(303);
}

// === СЕНСОРНАЯ КНОПКА (TTP223) ===
void handleTouch() {
  static bool wasPressed = false;
  static unsigned long lastPressTime = 0;

  bool isPressed = (digitalRead(TOUCH_PIN) == HIGH); // TTP223: HIGH при касании

  if (isPressed && !wasPressed) {
    if (millis() - lastPressTime > 300) {
      displayOn = true;
      screenOnUntil = millis() + 20000;
      drawScreen();
      lastPressTime = millis();
    }
  }
  wasPressed = isPressed;
}

// === SETUP ===
void setup() {
  Serial.begin(115200);
  nvs_flash_init();

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  pinMode(TOUCH_PIN, INPUT); // TTP223

  tft.initR(INITR_BLACKTAB);
  tft.setRotation(0);
  tft.fillScreen(ST7735_BLACK);
  tft.println("Starting...");

  WiFi.begin(ssid, password);
  uint8_t dots = 0;
  while (WiFi.status() != WL_CONNECTED && dots < 20) {
    delay(500);
    tft.print(".");
    dots++;
  }

  Serial.print("IP: ");
  Serial.println(WiFi.localIP());

  Wire.begin();
  if (!bme.begin(0x76)) {
    tft.println("BME280 ERROR!");
    while (1);
  }
  bme.setSampling(
    Adafruit_BME280::MODE_NORMAL,
    Adafruit_BME280::SAMPLING_X2,
    Adafruit_BME280::SAMPLING_X16,
    Adafruit_BME280::SAMPLING_X1,
    Adafruit_BME280::FILTER_X16
  );

  sensors.begin();
  int count = sensors.getDeviceCount();
  Serial.print("DS18B20 найдено: ");
  Serial.println(count);

  loadNames();

  server.on("/", handleRoot);
  server.on("/data", handleData);
  server.on("/settings", handleSettings);
  server.on("/save", HTTP_POST, handleSave);
  server.begin();

  readSensors();
  drawScreen();
}

// === LOOP ===
void loop() {
  server.handleClient();
  handleTouch();

  if (displayOn && millis() > screenOnUntil) {
    displayOn = false;
    drawScreen();
  }

  static uint32_t lastUpdate = 0;
  if (millis() - lastUpdate > 2000) {
    lastUpdate = millis();
    readSensors();
    if (displayOn) drawScreen();
  }
  delay(1);
}
внешний вид устройства без корпуса

Интеграция с умным домом

В Home Assistant добавляем REST-сенсор:

  1. Откройте файл конфигурации:
    Настройки → Система → Файлы конфигурации → configuration.yaml+
  2. Добавьте один блок sensor: для всех станций:
sensor:
  - platform: rest
    resource: http://IP_ДАЧИ/data
    name: "Meteo Station Dacha"
    json_attributes:
      - temperature
      - humidity
      - pressure
      - temp1
      - temp2
      - temp3

3. Создайте удобные сенсоры через template3

Добавьте в тот же файл:

# Сенсоры для дачи
template:
  - sensor:
      - name: "Температура в доме на даче"
        unique_id: meteo_bme280_temp_dacha
        state: "{{ state_attr('sensor.meteo_station_dacha', 'temperature') }}"
        unit_of_measurement: "°C"
        device_class: temperature

      - name: "Улица на даче"
        unique_id: meteo_ds18b20_1_dacha
        state: "{{ state_attr('sensor.meteo_station_dacha', 'temp1') }}"
        unit_of_measurement: "°C"
        device_class: temperature

Перезапустите HA

Затем создаём отдельные сенсоры для каждого параметра.

Шаг 4: Подключите к Алисе (через Yandex Smart Home)

  1. Установите интеграцию Yandex Smart Home (через HACS)
  2. В интерфейсе Yandex:
    Умный дом → Добавить устройство → По ссылке
  3. Скопируйте токен из HA
  4. Готово! Теперь можно спрашивать:«Алиса, какая температура в доме на даче?»
    «Алиса, скажи влажность в квартире»
Создаем узел сети

Добавление в Zabbix

Шаг 1: Создайте хост

  1. В Zabbix: Configuration → Hosts → Create host
  2. Заполните:
    • Host name: Meteo Station Dacha
    • Groups: IoT Devices
    • IP address: 192.168.189.10
    • Port: 80
создаем элемент данных
после ввода своих данных рекомендую нажать тест

Создайте график

  1. Graphs → Create graph
  2. Добавьте нужные элементы данных.

Заключение

Этот проект — не просто «умная игрушка». Это спокойствие. Вы знаете, что в доме всё в порядке. Вы видите погоду не по телевизору, а у себя на даче. И да — это невероятно круто, когда можно анализировать графики например зависимости влажности от температуры, или зависимость температуры в доме от температуры котла. Возможно это даже поможет сделать план по экономичному отоплению дома.

Добавить комментарий

Ваш адрес email не будет опубликован.