惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Cisco Blogs
D
Docker
The GitHub Blog
The GitHub Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
A
About on SuperTechFans
WordPress大学
WordPress大学
Recent Announcements
Recent Announcements
Engineering at Meta
Engineering at Meta
H
Help Net Security
Vercel News
Vercel News
S
SegmentFault 最新的问题
罗磊的独立博客
F
Full Disclosure
Microsoft Azure Blog
Microsoft Azure Blog
V
Visual Studio Blog
Last Week in AI
Last Week in AI
V
V2EX
腾讯CDC
IT之家
IT之家
爱范儿
爱范儿
博客园 - Franky
MyScale Blog
MyScale Blog
aimingoo的专栏
aimingoo的专栏
The Register - Security
The Register - Security
Help Net Security
Help Net Security
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
Cyber Attacks, Cyber Crime and Cyber Security
P
Proofpoint News Feed
D
Darknet – Hacking Tools, Hacker News & Cyber Security
B
Blog RSS Feed
雷峰网
雷峰网
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
Recorded Future
Recorded Future
A
Arctic Wolf
Cyberwarzone
Cyberwarzone
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog
S
Schneier on Security
GbyAI
GbyAI
Schneier on Security
Schneier on Security
O
OpenAI News
F
Fortinet All Blogs
Y
Y Combinator Blog
Forbes - Security
Forbes - Security
NISL@THU
NISL@THU
M
MIT News - Artificial intelligence
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
MongoDB | Blog
MongoDB | Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Мониторинг температуры и влажности в квартире на минималках
Levon24 · 2026-04-26 · via Все публикации подряд на Хабре

Данная статья является продолжением статьи 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 литров воды.

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