Данная статья является продолжением статьи https://habr.com/ru/articles/871380/ про сенсор HT2000, который измеряет CO₂, температуру и влажность. В рамках той статьи обсуждался более доступный вариант измерительного прибора. Конкретно этот вариант измерителя будет построен на датчиках типа AHT2X или SHT2X (в интернете полно информации об этих датчиках). Задача прибора-измерителя - измерять показания температуры и влажности и передавать их на сервер для хранения данных по домашнему Wi-Fi.

Внешний вид прибора
Внешний вид прибора

В качестве микроконтроллера был выбран ESP8266, собранный на плате NodeMCU v3. Микроконтроллер ESP8266 умеет передавать данные по Wi-Fi - собственно, это и требуется для онлайн системы мониторинга. Кроме того, модуль имеет программный интерфейс I²C для считывания показаний датчика и аппаратный интерфейс SPI для оперативного отображения показателей.

Распиновка модуля микроконтроллера
Распиновка модуля микроконтроллера

Датчик температуры и влажности

Ниже приведена сводная информация по наиболее популярным моделям датчиков серий AHT (Aosong) и SHT (Sensirion).

Параметр

AHT20

AHT21

AHT25

AHT30

SHT20

SHT30

SHT35

Производитель

Aosong (ASAIR)

Aosong (ASAIR)

Aosong (ASAIR)

Aosong (ASAIR)

Sensirion

Sensirion

Sensirion

Интерфейс

I²C

I²C

I²C

I²C

I²C

I²C

I²C

Напряжение питания

2.2 – 5.5В

2.2 – 5.5В

2.2 – 5.5В

1.8 – 3.6В

2.1 – 3.6В

2.15 – 5.5В

2.15 – 5.5В

Точность влажности (±% RH)

±2%

±2%

±1.8%

±2%

±3%

±2%

±1.5%

Точность температуры (±°C)

±0.3°C

±0.3°C

±0.2°C

±0.3°C

±0.3°C

±0.2°C

±0.1°C

Диапазон температур

-40 … +85°C

-40 … +85°C

-40 … +85°C

-40 … +85°C

-40 … +125°C

-40 … +125°C

-40 … +125°C

Диапазон влажности

0 … 100%

0 … 100%

0 … 100%

0 … 100%

0 … 100%

0 … 100%

0 … 100%

Особенности

Популярная бюджетная модель

Защита от конденсата

Повышенная точность

Сверхнизкое энергопотребление

Классическая надежная модель

Высокая скорость измерений

Премиальная точность

Я заказывал на AliExpress датчик AHT25, так как он, как видно из таблицы, более точный из серии AHT. Но что-то пошло не так, и мне привезли AHT30, поэтому я запаял его. Протокол общения у этих датчиков одинаковый.

Схема устройства

Ниже приведена принципиальная схема устройства. Через USB-интерфейс напряжение подаётся на модуль зарядки на микросхеме TP4056 и заряжает буферный аккумулятор. Далее напряжение поступает на модуль DC/DC-преобразователя, который питает всю остальную схему (датчик, микроконтроллер и дисплей). На плате микроконтроллера установлен сглаживающий конденсатор на 100 мкФ.

Принципиальная схема устройства
Принципиальная схема устройства

Модуль зарядки

Если у вас аккумулятор содержит модуль защиты то можно брать любой готовый модуль зарядки на микросхеме TP4056, однако я рекомендую взять все же с модулем защиты.

Модуль зарядки LiPol с защитой
Модуль зарядки LiPol с защитой

Ток зарядки у готовых модулей составляет 1 А, что может быть многовато для очень маленьких аккумуляторов, но его можно уменьшить заменой резистора, задающего ток зарядки на плате. Я такую модификацию не делал, поэтому не подскажу, какой именно резистор стоит заменить и на какой, подробнее можно глянуть datasheet на TP4056.

Подбор аккумулятора стоит начать с того, что готовое устройство потребляет средний ток 100 мА. Согласно документации, ESP8266 потребляет 170–300 мА (пиковые значения) во время передачи данных по Wi-Fi, в обычном режиме потребление составляет около 80 мА, а дисплей потребляет ещё 20 мА. Таким образом, несложной математикой можно рассчитать необходимое время работы устройства от аккумулятора. В моём случае я использовал старые аккумуляторы от ноутбука с потерянной емкостью, поэтому зарядный ток в 1 А меня устроил. У меня устройство работает от аккумулятора около 12 часов.

Я не использовал режим сна микроконтроллера, так как в этом не было необходимости. Однако лицензия проекта позволяет вам модифицировать код и реализовать такой функционал.

Важный недостаток данной зарядки в том, что при подключении к сети заряд аккумулятора всегда будет составлять 100%, что в конечном итоге приводит к его деградации. Альтернативой может быть зарядка до 85% или использование блокировочных диодов, но это уже выходит за рамки данного проекта.

DC/DC-преобразователь

Я брал готовый модуль, который умеет преобразовывать напряжение как вверх, так и вниз, поэтому у модуля две катушки индуктивности. Учитывая энергопотребление устройства я искал модуль, который выдает пиковый ток 330 мА (выше были представлены нагрузочные расчеты). В принципе, если у кого-то будет использоваться аккумулятор на 12 В, то такой модуль тоже должен подойти. Например: солнечная батарея на 12 В → зарядное устройство для соответствующего типа аккумулятора и далее этот модуль. В таком случае нужно будет пересчитать сопротивление резистора R1 чтобы не вывести из строя ADC преобразователь контроллера ESP8266.

DC/DC-преобразователь
DC/DC-преобразователь

Контроль заряда батареи

Модуль ESP8266 имеет на борту АЦП (аналого-цифровой преобразователь). На его вход можно подавать напряжение от 0 до 1 В. На модуле NodeMCU v3 распаян делитель напряжения, чтобы можно было подавать на вход от 0 до 3,3 В. В нашем случае нужно контролировать напряжение батареи от 0 до 4,2 В, поэтому был добавлен резистор R1 на 100 кОм, чтобы обеспечить такую возможность.

Датчик температура и влажности

Датчики, представленные на маркетплейсах, имеют разные конфигурации. В моем случае нужна минимальная конфигурация: блокировочный конденсатор для шины I²C и два подтягивающих резистора 4,7 кОм. Существуют также более продвинутые модели со стабилизаторами и двумя транзисторами для преобразования напряжения. Они тоже подходят - в моей конструкции использовался именно такой датчик.

Датчик температуры и влажности
Датчик температуры и влажности

Дисплей

В качестве дисплея я выбрал OLED-дисплей SSD1315 - это улучшенная версия SSD1306, и библиотечный драйвер от SSD1306 к нему подходит. Дисплей бывает в двух вариантах: с шиной I²C и с шиной SPI. Я выбрал второй вариант, так как у ESP8266 используется программный I²C, что может вызывать затормаживание при перерисовке экрана (в итоге к заметному мерцанию экрана), зато у контроллера ESP8266 есть аппаратный интерфейс SPI, что позволяет поднять скорость отрисовки экрана.

OLED дисплей
OLED дисплей

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

В качестве системы программирования был выбран Arduino, так как в нем есть все необходимые библиотеки из коробки. Проект опубликован тут https://github.com/Levon24/esp8266-temperature-humidity-monitor с открытой лицензией.

Сам измерительный прибор должен отправлять каждые 30 секунд данные через Wi-Fi на MQTT брокер, который может быть расположен как локально, так и в облаке. У меня в наличие был Orange Pi Zero 2W на котором я поднял локальный MQTT брокер для системы мониторинга.

В коде настройка MQTT следующая:

const char *mqttHost = "mercury.home.lan";
const int mqttPort = 1883;
const char *mqttTopic = "sensors/cabinet";
const char *mqttClientId = "esp-cabinet";

где mercury.home.lan это локальный сервер с брокером mqtt, порт стандартный 1833. Топик для отправки сообщений в моем случае sensors/cabinet так как датчик расположен в кабинете. Если вы будете использовать облачный сервис, то следует добавить авторизацию.

Измерительный прибор отправляет json сообщения с отметкой времени, данных о температуре и влажности, а также напряжения батареи. Пример сообщения:

{
  "timestamp": 1776431035,
  "temperature": 25.36640,
  "humidity": 19.60335,
  "battery": 3.92082
}

Для того чтобы выдавать отметку времени timestamp модуль обращается к ntp серверу для получения времени в UTC и далее сохраняет текущее время в переменную unixTime и ставит отсчет времени на 12 часов для последующей синхронизации.

    if (timeSync == 0 && timeClient.update()) {
      Serial.println("Sync time.");
      unixTime = timeClient.getEpochTime();
      timeSync = 6 * 1800; // 12 hours
    } else {
      if (timeSync > 0) {
        timeSync--;
      }
    }

Время между синхронизациями поддерживает внутренний единственный доступный железный таймер Timer, который можно использовать для такой работы (другой железный таймер используется для поддержания работы Wi-Fi). Устанавливаем его для срабатывания 1 раз в секунду.

  // Timer
  InterruptTimer.attachInterruptInterval(1000000, TimerHandler);

И далее производится отсчет секунд по таймеру переменной unixTime.

// Time
ICACHE_RAM_ATTR void TimerHandler() {
  unixTime++;
}

Код, который отправляет сообщений по MQTT достаточно простой, сам json формируется как строка. Стоит помнить, что в библиотеке стоит ограничение по-умолчанию на payload размером 128 байт, но его можно поднять, в случае необходимости.

if (pubSubClient.connect(mqttClientId)) {
  char payload[128];

  snprintf(payload, sizeof(payload), "{\"timestamp\": %ld, \"temperature\": %.5f, \"humidity\": %.5f, \"battery\": %.5f}", (long) unixTime, temperature, humidity, batteryVoltage);
  if (pubSubClient.publish(mqttTopic, payload)) {
    Serial.println("MQTT send");
    //delay(1000);
  }
  
  pubSubClient.disconnect();
}

Раз в 2 секунды показания отображаются на дисплее, тут тоже все достаточно просто.

display.clear();
display.setFont(ArialMT_Plain_10);

if (WiFi.status() == WL_CONNECTED) {
  display.drawString(0, 0, "IP: " + WiFi.localIP().toString());
} else {
  display.drawString(0, 0, "IP: Disconnected");
}

char text[30];
display.setFont(ArialMT_Plain_24);

snprintf(text, sizeof(text), "%.4f °C", temperature);
display.drawString(0, 10, text);

snprintf(text, sizeof(text), "%.4f %%", humidity);
display.drawString(0, 30, text);

display.setFont(ArialMT_Plain_10);

snprintf(text, sizeof(text), "Battery: %.2f V > %.2f %%", batteryVoltage, batteryPercent);
display.drawString(0, 52, text);

display.display();

Время в 2 секунды выбрано как рекомендованное время для опроса датчика AHT2X. Собственно ниже код, который читает показания датчиков:

  float temperature = aht20.getTemperature();
  float humidity = aht20.getHumidity();

  int batteryValue = analogRead(A0);
  float batteryVoltage = ((float) batteryValue) * (BATTERY_MAX / 1023.0);
  float batteryPercent = (batteryVoltage - BATTERY_MIN) * (100.0 / (BATTERY_MAX - BATTERY_MIN));

Настройка брокера MQTT

Для локального сбора данных телеметрии можно использовать MQTT-брокер. В качестве популярной реализации обычно используют Eclipse Mosquitto. Поскольку в моём распоряжении есть Orange Pi Zero 2W, я установлю брокер Eclipse Mosquitto именно на него.

Перед началом установки обновим систему:

sudo apt update && sudo apt upgrade -y

Далее установим необходимый пакеты:

sudo apt install mosquitto mosquitto-clients -y

По-умолчанию сервис слушает только локальный интерфейс, поэтому стоит добавить файл со следующим содержимым (1 строка это команда для вывода содержимого файла):

> sudo cat /etc/mosquitto/conf.d/default.conf
listener 1883 0.0.0.0
allow_anonymous true

Далее можно сервис перезапустить:

sudo systemctl restart mosquitto.service

По результату у нас будет готовый брокер сообщений, который можно проверить командой ниже:

Скрытый текст
$ sudo systemctl status mosquitto.service ● mosquitto.service - Mosquitto MQTT Broker     Loaded: loaded (/lib/systemd/system/mosquitto.service; enabled; preset: enabled)     Active: active (running) since Sun 2026-03-22 11:38:39 +04; 34min ago       Docs: man:mosquitto.conf(5)             man:mosquitto(8)    Process: 1136289 ExecStartPre=/bin/mkdir -m 740 -p /var/log/mosquitto (code=exited, status=0/SUCCESS)    Process: 1136291 ExecStartPre=/bin/chown mosquitto /var/log/mosquitto (code=exited, status=0/SUCCESS)    Process: 1136292 ExecStartPre=/bin/mkdir -m 740 -p /run/mosquitto (code=exited, status=0/SUCCESS)    Process: 1136293 ExecStartPre=/bin/chown mosquitto /run/mosquitto (code=exited, status=0/SUCCESS)   Main PID: 1136294 (mosquitto)      Tasks: 1 (limit: 4545)     Memory: 1.1M        CPU: 708ms     CGroup: /system.slice/mosquitto.service             └─1136294 /usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf
Mar 22 11:38:39 mercury systemd[1]: Starting mosquitto.service - Mosquitto MQTT Broker...
Mar 22 11:38:39 mercury mosquitto[1136294]: 1774165119: Loading config file /etc/mosquitto/conf.d/default.conf
Mar 22 11:38:39 mercury systemd[1]: Started mosquitto.service - Mosquitto MQTT Broker.

Для просмотра сообщений, можно использовать команду (1ая строка это команда):

> mosquitto_sub -t "#" -v
sensors/kitchen {"timestamp": 1777045575, "temperature": 23.31867, "humidity": 42.32216, "battery": 3.85513}
sensors/cabinet {"timestamp": 1777045596, "temperature": 20.14351, "humidity": 53.22123, "battery": 4.20411}
sensors/kitchen {"timestamp": 1777045604, "temperature": 23.26317, "humidity": 42.35744, "battery": 3.85513}
sensors/cabinet {"timestamp": 1777045625, "temperature": 20.07809, "humidity": 52.88181, "battery": 4.20411}

Однако на данном этапе, скорее всего вы не увидите никакой информации, т.к. нужно подписаться на данные топики, чтобы MQTT брокер смашрутизировал данные. Для этого нужно добавить подписчика, который будет зачитывать данные и писать их уже в базу данных, о чем будет рассказано ниже.

ESP-Sensors Consumer

Сам проект находится по ссылке https://github.com/Levon24/esp-sensors-consumer и аналогичен проекту https://github.com/Levon24/HT2000 который описан в статье https://habr.com/ru/articles/871380/ но вместо чтения данных из usb данные читаются из MQTT брокера.

Данный сервис поднимается на сервере с базой данных для grafana, у меня это не тот же сервис, где находится MQTT брокер, а тот, где расположен датчик с HT2000. Я так сделал для своего удобства, вы же можете все на одном сервере разместить. Ну как сервере, это железка Orange Pi Zero 2W размером 65 на 30 мм и минимальной стоимостью.

Чтением данных из брокера занимается сервис EspSensorsService, в нем, для первой версии, прописаны в коде топики и сенсоры для сохранения данных в базу данных.

  private static final Map<String, String> sensors = Map.of(
    "sensors/kitchen", "kitchen",
    "sensors/cabinet", "cabinet"
  );

В последующих версиях я планирую сделать конфигурируемым этот маппинг, но для PoC решил оставить пока так, чтобы не усложнять сервис. Чтение данных из MQTT брокера построено по асинхронной архитектуре и добавлена поддержка пере-подсоединения:

  @PostConstruct
  public void init() throws MqttException {
    mqttClient = new MqttClient(brokerUrl, clientId, new MemoryPersistence());
    mqttClient.setCallback(this);

    final MqttConnectOptions connectOptions = new MqttConnectOptions();
    connectOptions.setCleanSession(true);
    connectOptions.setAutomaticReconnect(true);
    connectOptions.setKeepAliveInterval(60);
    logger.info("Connecting to broker: {}.", brokerUrl);

    mqttClient.connect(connectOptions);
    logger.info("Initialized");
  }

Как только данные приходят, сервис кладет из в базу данных:

  @Override
  public void messageArrived(String topic, MqttMessage message) {
    final String payload = new String(message.getPayload());
    logger.info("MessageArrived: {} on topic: {}.", payload, topic);

    try {
      final EventDto eventDto = objectMapper.readValue(payload, EventDto.class);
      logger.debug("Message EventDto: {}.", eventDto);

      final Event event = new Event();

      final Timestamp timestamp;
      if (eventDto.timestamp() != null && eventDto.timestamp() > 0) {
        timestamp = new Timestamp(eventDto.timestamp() * 1000);
        logger.debug("Timestamp: {}.", timestamp);
      } else {
        timestamp = new Timestamp(System.currentTimeMillis());
      }
      event.setTimestamp(timestamp);

      final String sensor = sensors.get(topic);
      event.setSensor(sensor);

      event.setTemperature(eventDto.temperature());
      event.setHumidity(eventDto.humidity());
      event.setBattery(eventDto.battery());

      final Event s = eventService.save(event);
      logger.info("Saved event: {}.", s);
    } catch (Exception e) {
      logger.warn("Convert - Error: {}.", e.getMessage(), e);
    }
  }

Обратите внимание, что поле timestamp может не прийти, т.к. я планирую разработку датчиков, которые будут передавать показания через BLE протокол, я уже закупил пару контроллеров CH592F для изучения их в свободное время. Собственно предполагается, что у контроллеров с BLE не будет доступа к сервису NTP для получения точного времени.

  @Override
  public void messageArrived(String topic, MqttMessage message) {
    final String payload = new String(message.getPayload());
    logger.info("MessageArrived: {} on topic: {}.", payload, topic);

    try {
      final EventDto eventDto = objectMapper.readValue(payload, EventDto.class);
      logger.debug("Message EventDto: {}.", eventDto);

      final Event event = new Event();

      final Timestamp timestamp;
      if (eventDto.timestamp() != null && eventDto.timestamp() > 0) {
        timestamp = new Timestamp(eventDto.timestamp() * 1000);
        logger.debug("Timestamp: {}.", timestamp);
      } else {
        timestamp = new Timestamp(System.currentTimeMillis());
      }
      event.setTimestamp(timestamp);

      final String sensor = sensors.get(topic);
      event.setSensor(sensor);

      event.setTemperature(eventDto.temperature());
      event.setHumidity(eventDto.humidity());
      event.setBattery(eventDto.battery());

      final Event s = eventService.save(event);
      logger.info("Saved event: {}.", s);
    } catch (Exception e) {
      logger.warn("Convert - Error: {}.", e.getMessage(), e);
    }
  }

Разворачивание начальной структуры базы данных сделано аналогично как и в проекте HT2000, сам скрипт на разворачивание базы данных находится тут https://github.com/Levon24/esp-sensors-consumer/blob/master/scripts/init.sql. После инициализации базы данных, будет запущен скрипт для создания единственной таблицы с событиями.

Grafana

Установка и настройка Grafana изначально производились для прибора HT2000, что было описано в статье https://habr.com/ru/articles/871380/, и здесь я не вижу особого смысла повторять материал. Единственный момент: если всё было настроено для HT2000, то для первоначальной настройки необходимо добавить новый DataSource «esp-sensors» для базы данных MariaDB. Далее можно загрузить готовые графики, например, из файла https://github.com/Levon24/esp8266-temperature-humidity-monitor/blob/master/Docs/esp-sensors.json, либо настроить их вручную.

Отображение графиков
Отображение графиков

Для ручной конфигурации я приведу пример запроса на температуру для датчика, расположенного на кухне.

SELECT $__timeGroup(timestamp, '1m') AS time, avg(temperature) AS value, min(temperature) AS min, max(temperature) AS max
FROM events
WHERE $__timeFilter(timestamp) 
  AND sensor = 'kitchen'
GROUP BY time

Наверное, можно создать шаблоны и использовать их, но я пока не разобрался как это делать. Я использовал функцию клонирования.

Итоги

Данная статья — это попытка реализовать мониторинг квартиры с использованием относительно недорогих чипов. Необходимость в таком мониторинге возникла после покупки недорогих метеостанций и понимания, что их показания недостаточно точны — это выяснилось в ходе экспериментов с увлажнителями. Потребовались более точные результаты. Так и появился данный проект проверки концепции, на что-то большее я и не рассчитывал.

В статье рассмотрена только техническая составляющая измерительного комплекса и не затронута тема влажности воздуха и сухости кожи, а также их влияния на здоровье человека в отопительный сезон. После рождения дочери возникла задача быстро собрать такой мониторинг, поскольку простые бытовые модели за 100–300 рублей постоянно показывали «LL» на индикаторе (Low Level), и ориентироваться по ним было сложно.

Я также сделал вывод, что недорогие ультразвуковые увлажнители, несмотря на обильное визуальное выделение пара, недостаточно сильно увлажняют воздух. Именно поэтому на индикаторе требуется такая точность в процентах — чтобы улавливать незначительные изменения параметров. Небольшая батарея из 2–3 ультразвуковых увлажнителей не заменит хорошую мойку воздуха, которая за ночь способна испарить до 5–10 литров воды.

Берегите себя, всем мира и добра!

Комментарии (5)


  1. DirOr
    26.04.2026 04:56

    Не оспаривая тезис "... недостаточно сильно увлажняют воздух. Именно поэтому на индикаторе требуется такая точность в процентах ", все же замечу, что значение на индикаторе температуры и влажности с 4 знаками после запятой - явный перебор.

    По объективным причинам.

    Я бы сократил до десятых, т.е. одного знака после запятой... И то, надо понимать, что верить этим значениям надо с оговоркой.


    1. Levon24 Автор
      26.04.2026 04:56

      Согласен, учитывая, что точность у моего датчика по температуре ±0.3°C и ±3% по влажности. Мне же хотелось при включении рядом увлажнителя видеть эффект от его работы, на сколько он справляется в этих "попугаях". На сколько влажность изменяет в метре и далее.


  1. foDo
    26.04.2026 04:56

    Не согласен с выводом про 2-3 увлажнителя и 5-10 литров воды.

    У меня стоит один увлажнитель 4.5 литра (цена 4000₽) в центре квартиры и за 7 часов он весь испаряется. В холодные дни включаю увлажнитель в спальне (цена 3000₽). У него объем 6 литров и за ночь уходит примерно 2.5. в сумме получается около 7 литров. А если заменить увлажнитель в спальне на тот, который 4.5 литра, то объем будет приближаться к 10л.


    1. Levon24 Автор
      26.04.2026 04:56

      А можно узнать что у вас за модели увлажнителей? Видимо я какие-то не те брал, в основном на 2 литра объемом с 2-3 пьезо-испарителями.


      1. foDo
        26.04.2026 04:56

        Да, конечно. Тот что самый мощный вроде как этот (сомнения, потому что как-то многовато этих Xiaomi одинаковыми названиями и разными характеристиками): https://ozon.ru/t/OLEs8ar

        Второй менее мощный и более тихий (стоит в спальне) вот такой: Levoit Classic 200

        Пока писал, оказалось у него не 6 литров, а 4. Получается, что чуть меньше реальные числа по литрам. Но в любом случае, если вы купите 2 Xiaomi, то будет 9 литров за ночь.