Помните это чувство? Вы просыпаетесь ранним утром на даче, смотрите в окно — и думаете: «А не замёрз ли картофель в подвале?» Или: «Не перегрелся ли котёл, пока я спал?» Раньше я просто бегал с термометром по дому. А потом собрал умную метеостанцию на 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 VCC | 3.3V |
| TFT GND | GND |
| TFT LED | GPIO26 |
| TFT SCK | GPIO18 |
| TFT MOSI | GPIO23 |
| TFT CS | GPIO5 |
| TFT DC | GPIO16 |
| TFT RST | GPIO17 |
| BME280 SDA | GPIO21 |
| BME280 SCL | GPIO22 |
| DS18B20 (все три) DATA | GPIO4 + резистор 4.7 кОм к 3.3V |
| TTP223 OUT | GPIO14 |
Программная часть
Устанавливаем 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-сенсор:
- Откройте файл конфигурации:
Настройки → Система → Файлы конфигурации → configuration.yaml+ - Добавьте один блок
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)
- Установите интеграцию Yandex Smart Home (через HACS)
- В интерфейсе Yandex:
Умный дом → Добавить устройство → По ссылке - Скопируйте токен из HA
- Готово! Теперь можно спрашивать:«Алиса, какая температура в доме на даче?»
«Алиса, скажи влажность в квартире»

Добавление в Zabbix
Шаг 1: Создайте хост
- В Zabbix: Configuration → Hosts → Create host
- Заполните:
- Host name:
Meteo Station Dacha - Groups:
IoT Devices - IP address:
192.168.189.10 - Port:
80
- Host name:


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

