"Мне нужен не умный дом, мне нужен послушный дом" - / из Интернета /

Хотя концепция "умного дома" в полном объёме мало кем используется в жизни (и ещё меньше, кем самостоятельно реализована), отдельные его компоненты - разного рода датчики, умные розетки, камеры и прочее, с доступом через "облако" - давно не редкость. До покупки дачи меня всё это слабо волновало - в квартире я вполне обходился механическими выключателями и градусником на окне. Тем не менее, необходимость следить за состоянием загородного дома вынудила заняться этим вопросом и я пошёл по самому простому пути с MiHome и кучей датчиков. Конечно, в таком решении хватает проблем - баги, которые не устраняют годами, датчики для разных регионов, периодически [кратковременно] отваливающееся облако. В целом, однако, всё это удовлетворительно работает уже не первый год, технического интереса не представляет и рассказать я хотел не об этом.

Вопрос, который меня всерьёз стал беспокоить уже на вторую зиму (а именно зима - самый проблемный сезон) - необходимость в резервном решении на случай, если информация от MiHome перестанет поступать и надо будет решать - нужно ли срочно ехать и топить/тушить/чинить дом или можно ещё подождать, пока проблема сама рассосётся. Основных причин прекращения поступления информации три:

  1. Ситуация длительного, либо многократного отключения электричества (в теории - на 72 часа, хотя пока максимум было около 48) и разряда аккумуляторов бесперебойника (инвертора), который у меня распространяется на все маломощные потребители.

  2. Пропадание Интернета - используемое оптоволоконное подключение подвержена тем же проблемам с ветром, снегом, деревьями, что и электричество.

  3. Возможные длительные проблемы с облаком MiHome, либо его приложением (последнее особенно вероятно, т.к. приходится пользоваться неофициально модифицированной версией, позволяющей использовать датчики для разных регионов)

Воображение уже рисует комментарии с вопросами "А почему было не поставить Home Assistant?". Ну, изначально я так и хотел. Даже купил Raspberry Pi 4B, накатил HA и убедился, что с парой каких-то датчиков это всё работает. Однако, в процессе настройки я осознал, что такое решение - скорее замена MiHome (который меня, в целом, устраивает). Для резерва на случай временной аварии, HA - явный перебор. Причём, перебор не столько в плане функционала (это ещё ладно), а в плане сложности - достаточно уже одного того, что это всё работает поверх ОС и через кучу драйверов, которые неизвестно как и в какой ситуации себя будут вести. Мне же надо как можно тупее, проще, без лишних прослоек. И ещё важно, чтобы поменьше потребляло.

Поскольку я в прошлом немного игрался с STM32 (чисто по-любительски), меня пару раз посещала мысль реализовать нужный функционал на базе какого-нибудь микроконтроллера из этой линейки. Но, честно говоря, объём работ меня слегка пугал. Особенно с учётом, что чипы со встроенным Wi-Fi/BT у них появились сравнительно недавно и dev boards с ними стоят как-то негуманно. Конечно, краем уха я слышал про ESP8266 / ESP32, но оно у меня вызывало нездоровые ассоциации со словом Arduino (забегая вперёд - зря).

В итоге, наткнувшись в очередной раз на какую-то страничку про ESP32, я вчитался повнимательнее и с удивлением узнал что, оказывается, на маленьком недорогом модуле есть одновременно и BT и Wi-Fi и достаточно портов под всё, что мне надо. Это сподвигло меня наконец заняться решением вопроса - вдобавок ещё погода такая мерзкая была, нос из дома высовывать никакого желания. У кого не хватает мотивации что-то написать, рекомендую - переезжайте в Питер.

По-правильному (а ещё, чтобы не называли ардуинщиком) следовало конечно поставить ESP-IDF но, посмотрев наискосок, как это выглядит - мне стало лень. Опять настраивать vscode под очередную платформу и копаться в cmake файлах - не хочу. Хочу решить конкретную задачу. А самореализовываться переставлением битиков и копанием в конфигах предпочитаю на чём-нибудь другом.

Короче, дав слабину, поставил Arduino IDE. Это не VSCode конечно - заметно, что авторы сами не пользуются тем, что разрабатывают, однако, если честно, я ожидал худшего. Достаточно быстро удалось по-отдельности убедиться, что через Wi-Fi данные отправляются, информация с разных датчиков снимается (для обучения удобно использовать симулятор ESP32 - wokwi. Там даже Wi-Fi имитируется).

Поначалу я себе представлял задачу предельно просто - хотел ограничиться замером температуры и напряжения до и после ИБП (инвертора), а принимать и показывать информацию простеньким скриптом на PHP. Однако, в процессе того, как я разбирался с ESP32, аппетиты росли прямо на глазах. В результате минимальные требования к устройству изменились. Мне захотелось:

  1. Получения данных о температуре внутри устройства, рядом с устройством, за стенкой от устройства (на улице). Для этого я выбрал onewire датчики DS18B20 (один на плате, два с проводами 1-2 метра).

  2. Получения данных о температуре и влажности рядом с трубами на полу в другой комнате и на полу рядом с аккумуляторами ИБП - в третьей комнате (куда проводами уже не дотянуться). Для этого я использовал популярные bluetooth датчики LYWSD03MMC, предварительно перешитые другим firmware.

  3. Измерение напряжения в сети 220в до и после ИБП при помощи модулей ZMPT101B (там трансформатор с усилителем).

  4. Определение наличия человека в комнате, где расположено устройство. PIR датчик HC-SR501.

  5. Определение замыкания/размыкания геркона (установленного на дверь). Геркон с магнитом подключён непосредственно к порту МК.

  6. Реле, которым можно управлять, меняя ответ сервера на запрос устройства.

Схема

Измерения периодически, раз в несколько минут, планировалось отсылать через Wi-Fi на сервер, обычным POST запросом с json содержимым (и да, я знаю о существовании MQTT).

Для сети Wi-Fi используется отдельный мобильный роутер ZTE MF910 с симкартой на самом дешёвом тарифе для "интернета вещей".

И устройство и роутер питаются по USB от аккумулятора, который постоянно подзаряжается от блока питания включённого в 220 в. Таким образом, получается своего рода второй независимый ИБП, о котором надо сказать особо.

Дело в том, что обычный среднестатистический power bank не может без вмешательства оператора работать в режиме ИБП - т.е. обеспечивать сквозное питание устройства с переключением на внутренний аккумулятор при пропадании питания. Тем не менее, некоторые всё же могут, причём информация об этой возможности не скажу, что легко доступна - думаю, производители не особо заинтересованы её афишировать, т.к. подобный режим для литиевых аккумуляторов не слишком полезен.

Сначала я купил power bank под названием Soshine E3S. Между прочим, довольно интересное и неплохое устройство. По-видимому, больше не производится, но найти в продаже можно.

Нужный мне режим работы оно обеспечивает (более того, там на индикаторе прямо так и пишется про "UPS mode"). Однако, уже где-то в середине разработки я случайно увидел в продаже нечто под названием "UPS Power Module (B) для Jetson Nano" и решил, что это более удачный вариант. Особенно мне понравилось, что оно умеет отдавать по I2C информацию о состоянии ИБП (там внутри INA219) - пустяк, а приятно. Soshine остался в качестве резервного варианта, тем более что и там и там используются четыре стандартных аккумулятора 18650.

Делать печатную плату я не стал, иначе бы проект затянулся на совсем неопределённое время - взята обычная макетка с отверстиями и соединения выполнены проводами из витой пары.

В плане корпуса меня волновало следующее:

  1. Если всё это добро (особенно, аккумуляторы) воспламенится, это не должно привести к пожару

  2. Внутрь корпуса должны влезать вышеупомянутый ИБП на 4x18650, само устройство и Wi-Fi-GSM роутер

  3. При этом содержимое не должно слишком переохлаждаться и слишком перегреваться

  4. Корпус, понятно, не должен быть металлическим, чтобы роутер работал

  5. Если сверху потечёт вода (когда падающее дерево или метеорит в дождливую погоду пробьёт крышу), внутрь она попасть не должна

В итоге была взята большая пластмассовая распределительная коробка 150 x 110 x 70 и между ней и стеной дополнительно проложен лист огнеупорного гипсокартона.

Устройство в сборе, на стене (ИБП позади платы, на колонках)

Вернёмся к программной части. Порадовало, что легко нашлись полностью или частично готовые решения для работы со всеми датчиками и ответы на все возникающие по ходу вопросы. Не могу сказать, что качество библиотек вот прямо идеальное, но для моих целей годное.

Алгоритм первоначально был незатейливым - каждые N минут снимаем показания температуры и напряжения и сразу шлём их на сервер. Что касается геркона и PIR датчика, то их срабатывание вызывает обработчик прерывания, в котором соответствующий датчику флаг устанавливается в true и его значения отсылается в своё время, вместе с температурой и напряжением.

Первым делом я сделал отсылку тестового json'а POST запросом по Wi-Fi с использованием библиотеки WiFiClientSecure (Secure - это с поддержкой SSL. Кстати, если лень заморачиваться с сертификатом, можно это решить через client.setInsecure() ).
JSON можно было бы сформировать руками, но мне что-то приспичило сделать аккуратно, для чего я выбрал ArduinoJson v7.
Кроме, собственно, отсылки данных, я поначалу добавил ещё синхронизацию часов по NTP, чтобы отсылать время. Но потом убрал, т.к. практической пользы эта информация не несёт.

Вторым по значимости я счёл получение информации от BT датчиков. И с этим пришлось повозиться. Задумка была такой - сканируем всё BLE устройства, которые отзываются, из них отбираем те, у которых serviceUUID = 0x181a (Temperature and Humidity sensor). Таким образом, для добавления нового датчика не потребуется трогать устройство - данные будут поступать со всех доступных.

Обычно в такой ситуации используют библиотеку BLEDevice. С одной стороны, она безусловно работает. Но вскоре обнаружилось, что при опросе в цикле начинает течь память. Сначала думал, что я тормоз (ввиду минимального опыта программирования на C, эта версия казалась наиболее правдоподобной), но через пару дней возни наткнулся на жалобы других людей на аналогичные симптомы в очень похожей на мою ситуации. В итоге перешёл на библиотеку NimBLEDevice (за основу взят этот пример) и, о чудо, память течь перестала!

Я уже было обрадовался и оставил код несколько дней настояться. getFreeHeap() отсылался в каждом HTTP запросе и я видел, что с памятью проблем нет. Но, где-то в середине вторых суток сканирование вдруг стало возвращать 0 устройств и сопровождаться ошибкой "scan_evt timeout". RESET помог, но через день или два история повторилась снова. Вновь углубившись в гугленье, снова обнаружил себя в компании таких же несчастных. Причём, их было намного больше, чем предыдущих (с текущей памятью). Насколько мне удалось понять, проблема тут серьёзнее и уже не в библиотеке, а в реализации BT стека в самом ESP32. Проблеме не один год - проявляется она не у всех (вероятно, мало кто в цикле непрерывно сканирует BLE устройства, да ещё сутками). В итоге, вопрос был решён примитивным if (count==0) esp_restart();

Кстати, если нужно получать имена устройств, setActiveScan должен быть вызван с true.

Отдельно упомяну, что когда я объединил сканирование BLE с отправкой данных по WiFi, результат поначалу не влез в прошивку (у меня ESP32 с 4MB Flash - насколько я знаю, это максимум из широкодоступного). Это решается так: Tools / Partition scheme / No OTA (large app).

С остальными датчиками, которые подключены непосредственно к портам ESP32, проблем не возникло - это известные модули и в сети есть множество примеров. Поскольку у ESP32 оставались ещё незадействованные порты, я приделал реле, которое включается, если на POST запрос сервер возвращает {"relay":true}. Аналогичным образом сделано изменение периода опроса и принудительный сброс устройства.

В первых версиях фиксировались данные с PIR датчика и данные с геркона. Тем не менее, спустя какое-то время я отказался от геркона (добавив вместо него второй PIR датчик). За этой, казалось бы, мелочью, скрывается довольно много потраченных нервов и времени. Фиксация замыкания простого контакта была последним, от чего я ожидал сложностей, а вышло всё ровно наоборот. Поначалу ничто не предвещало - на столе всё замечательно стабильно работало (у меня был настроен INPUT_PULLUP на GPIO25, повешен обработчик прерываний на CHANGE для этой ноги). Первые звоночки, в прямом и переносном смысле, появились, когда я повесил уже отлаженное устройство на стену, воткнув в него все датчики, включая подачу 220в на трансформаторы. Изредка я стал замечать ложные срабатывания. Уж не знаю, сыграл ли роль провод к геркону длиной несколько метров или присутствие трансформаторов 220в прямо рядом с ESP32. Отлаживать уже установленное устройство на даче мне было лень и я, до поры до времени, махнул на эту проблему рукой - тем более, эти датчики были скорее приятным бонусом, а не основным функционалом.

Спустя несколько недель у меня появилось нездоровое желание отказаться от прерываний (на которых висел этот геркон и PIR датчик) и сделать, чтобы устройство всё время спало и просыпалось:

а). через заданные промежутки времени, чтобы отправить данные о температуре и напряжении;
б). немедленно, при замыкании/размыкании геркона и сигнале от PIR датчика, отправляя данные вне очереди.

Заодно я отказался от первоначальной схемы работы с WiFi, когда соединение устанавливалось один раз и возобновлялось лишь при обнаружении разрыва соединения - в новом варианте соединение заново устанавливалось каждый раз перед отправкой данных (ну, в случае со сном иначе и быть не могло).

С deep sleep я заморачиваться на стал, т.к. при этом стирается память и пришлось бы возиться с сохранением данных в RTC. Остановился на light sleep и ext1 режиме, позволявшим просыпаться при возникновении высокого уровня на любой из заданных ног. Примерно так:

esp_sleep_enable_timer_wakeup(requestPeriod * 1000000); 

esp_sleep_enable_ext1_wakeup( GPIO_SEL_xx | GPIO_SEL_yy , ESP_EXT1_WAKEUP_ANY_HIGH );  

esp_light_sleep_start();  

[...спим....]

gpio_wakeup = (log(esp_sleep_get_ext1_wakeup_status()))/log(2) ;

[...проснулись и выяснили, за какую ногу нас дёргали (или это просто будильник зазвонил...]

Первое, с чем я столкнулся - просыпаться можно только если нас дёргают за вполне определённые ноги (присоединённые к RTC). PIR датчик у меня на такой и висел, а вот геркон нет. Пришлось всё вытаскивать и перепаивать.

Дальше обнаружилось, что сигнал на ноге появляется даже если просто подходишь к столу. Т.е. она явно floating, несмотря на установленный pinMode. Изучение вопроса показало, что в спящем режиме внутренние резисторы не работают. Ладно, припаял снаружи - всё стало срабатывать четко, НО - только если читать ногу через digitalRead(). Если же использовать esp_sleep_enable_ext1_wakeup() то, такое впечатление, что за эту ногу его дёргают непрерывно. Причём, что интересно - esp_sleep_get_ext1_wakeup_status() возвращает разную ерунду - т.е. либо 0, либо какие-то странные значения. Но при замыкании геркона - возвращает верное значение. Я уже изолировал всё это в отдельный исходник, выключил просыпание по таймеру, оставил только просыпание по этой одной ноге (GPIO26), ничего не изменилось - происходит то чего, по идее, в принципе не может происходить.

Полез смотреть, что пишут в Интернете. Нашёл описание аналогичной проблемы, но во всех двух или трёх случаях вопрошавшие внятного ответа не получили. Наиболее осмысленное пояснение которое я вычитал - что такой эффект может наблюдаться при помехах, либо сигнале на ноге, который выходит за пределы допустимых значений. Немного поэкспериментировав с резисторами я в итоге решил, что если я сейчас не остановлюсь, этот проект станет бесконечным. И вместо геркона прицепил для успокоения второй PIR датчик (с ним всё работает чётко).

Тут надо признаться, что всё эти пляски со сном были, по большому счёту, излишними - общее потребление связки "устройство + wifi роутер" упало совсем незначительно. Т.е. до переделки это всё работало от четырёх 18650 38.5 часов, после добавления сна - 40.3 часа.

Потребление роутера около 0.15A (постоянно). Что касается устройства, то до переделки на сон потребление выглядело так (увеличение тока - это отправка данных):

После - не было возможности измерить.

Сервер

Поначалу я набросал "на коленке" пару простых PHP скриптов, один из которых принимал json от устройства и клал его в postgres базу, а второй выводил данные в табличках. Убедившись, что всё работает плюс-минус как планировалось, решил заморочиться с чем-то более правильным и красивым (да, я знаю что лучшее - враг хорошего).

Как сказано выше, данные хранятся в Postgres базе. Всё достаточно примитивно - в основную табличку пишется общая информация по каждому POST запросу от устройства - номер устройства, состояние аккумулятора, WiFi RSSI, heap (чтобы видеть, не течёт ли память) и т.п.

В отдельных табличках, с привязкой к основной, данные с конкретных датчиков - температура, влажность, напряжение в сети и пр.

Поскольку предполагается, что в будущем устройств будет больше одного, и может быть не только на даче, предусмотрены табличка st_locs для мест и st_srcs для устройств (для упрощения отладки они в таблицах пока продублированы). В st_srcs для каждого устройства есть столбцы для управления - время между отсылками данных, состояние реле и прочее.

Сервер реализован на nodejs / express и наружу торчат три endpoint'a:

'/graphql' - Данные для веб приложения (которое рисует виджеты/графики и имеет интерфейс для передачи настроек устройству) сервер отдаёт в виде graphql с использованием postgraphile.
По сути, postgraphile даёт возможность делать запросы к postgres через graphql. Это удобно - одной строчкой app.use(postgraphile()) получаем почти весь нужный функционал.
Причём, queries и mutations он создаёт из postgres схемы сам и автоматически обновляет их при, скажем, переименовании таблиц или добавлении в них столбцов.
Правда, этого достаточно только для типовых случаев вида "получить строки из таблицы A и связанных с ней таблиц B и C где поле x = значение, с сортировкой". Если надо добавить или изменить строку в таблице по заданому id, это тоже типовой случай. А вот если условие чуть посложнее или надо поместить значение не в одну таблицу, а ещё и в связанные с ней - тут уже придётся написать хранимую процедуру, которую postgraphile тоже автоматически увидит и позволит использовать в виде custom mutation с именем соответствующим этой функции.

конструктор запросов и ответ (справа) в отладочном интерфейсе Postgraphile

'/api' - Данные от самого устройства приходят сюда в виде json. Формировать graphql запросы на esp32 мне показалось излишне замороченным (хотя, сейчас я уже начал в этом сомневаться). В результате, нужно было либо из этого обработчика как-то обращаться к postgraphile (что довольно нетривиально), либо использовать обычные SQL запросы через pg. Я пошёл наиболее простым - вторым путём.

Сначала полученные данные кладутся в базу, затем оттуда, из st_srcs, достаются параметры, которые мы хотим передать устройству в ответ (нужно ли его сбрасывать, нужно ли включить реле, желаемое время между передачей нам данных).

'/' - Последняя функция nodejs сервера - отдавать html/css собранного react приложения. Просто appWeb.use(express.static("public"));

Веб приложение

Здесь можно видеть, в том числе, разряд ИБП и два отключения устройства

Достаточно типовой подход с использованием React, MUI, Apollo Client и Recharts.

Поскольку данные поступают от устройства через заданные промежутки времени, нет смысла заморачиваться с subscription, web sockets и прочим. Apollo client просто периодически (в два раза чаще, чем они приходят от устройства на сервер) забирает данные за последние N часов.

Для каждого датчика показываются виджеты с иконкой, значением параметра и описанием. Плюс куча графиков изменений этих параметров. На самом деле их больше, чем нужно (как пошутил знакомый: "уровень мониторинга - АЭС"), но мне тут помимо прочего важно было оценить корректность работы устройства.
В любой точке графика наведением мышки можно смотреть численные значения.

Для каждого виджета определены диапазоны значений, считающихся нормой. При выходе из этих диапазоовы на виджете появляется красная отметка. Если данных от устройства долго (дольше установленного периода отсылки информации) нет, на графиках отмечаются соответствующие места, с указанием пропущенного времени.
Пунктирность линий от BT датчиков связана с тем, что не всегда в момент сканирования датчики выдают в эфир свою информацию.

Исходники (клиент, сервер, прошивка) - https://github.com/petersobolev/dsensors

Хотя всё это пока в опытной эксплуатации (я периодически меняю то сервер, то клиент, то прошивку, добавляя новые баги), в целом картина уже ясна. Всё работает вполне устойчиво. Очень удобно сразу видеть общую картину происходящего и прогнозировать ситуацию - что, собственно, и требовалось.

Некоторое беспокойство у меня вызывает температура внутри корпуса устройства. Если зимой, как показала практика, она почти наверняка будет выше нуля, то сейчас (конец мая) я уже однажды фиксировал максимальное значения +32 (хотя обычно не более +27). В жаркую летнюю погоду температура может стать проблемой для аккумуляторов, решить её будет непросто. Если сделать отверстия, зимой это всё может переохладиться. Т.е. по-хорошему нужен клапан перекрывающий вентиляционные отверстия. Не хочу об этом пока думать.

Из возможных усовершенствований:

  • Сделать больше одного устройства (например, добавить мониторинг расхода воды и общей потребляемой домом мощности)

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

  • Вместо ZTE роутера использовать отдельный GSM модуль - таким образом избавиться от идиотской передачи данных по WiFi на расстояние полтора сантиметра и существенно увеличить время автономной работы. Очень сооблазнительно, но у меня эта WiFi сеть используется ещё и как резервная, а не только лишь для этого устройства.

  • Разобраться всё-таки со стабильным определением срабатывания геркона. Может опторазвязку сделать?

  • Сделать, чтобы веб приложение забирало с сервера только новые данные (особого смысла нет, т.к. весь пакет, чтобы увидеть полные данные за сутки занимает около 100кб)

  • Сделать уведомления на email или telegram о выходе значений за пределы диапазонов

  • Отсылать информацию о состоянии обоих Интернет каналов (если пропадёт один, отсылать через второй и наоборот).

Эпилог

Хотя проект делался исключительно для себя, пара проходящих мимо людей высказались в духе: "А почему бы это всё не продавать?", "А мне такое можешь сделать?".

Никакой новизной и оригинальностью мои решения, ясное дело, не отличаются, однако, несмотря на это, вопросы вполне логичны. Дело в том, что среднестатистическому человеку не нужны решения вида MiHome, HA и подобные им - с кучей беспроводных датчиков, выключателей и мрачно замороченных настроек (вдобавок, не отличающиеся стабильной работой).
Задача, которую волнует большинство - иметь представление о трёх-четырёх основных параметрах и смотреть статистику / получать уведомления по их изменениям.

В этом плане идеальное устройство должно выглядеть как единый небольшой модуль, который втыкается в 220в, содержит внутри себя аккумулятор, WiFi или GSM, несколько датчиков, камеру с IR подсветкой, пару разъёмов для подключения внешних OneWire термометров и одну или две управляемых розетки 220в. Всё. Послушный дом мечты.

Тут, однако, возникает проблема - в таком бизнесе [по моему убеждению] сложность заключается вовсе не в проектировании и производстве такого устройства и даже не в написании софта для него. Основная сложность в том, что Интернет, увы, так устроен, что просто передать несколько байт между двумя компьютерами, которые к нему подсоединены - гигантская проблема. И продающий такие устройства должен будет ещё и обеспечивать круглосуточную гарантированную работоспособность сервера, который для этого потребуется. Да ещё и поддерживать его долгие-долгие годы, так как иначе устройства у счастливых покупателей превратятся в тыкву, что вызовет сильное их недовольство и, как следствие, боль продавца.

В IT все привыкли к такому положению вещей и воспринимают это как данность - кто-то берёт себе фиксированный IP, кто-то колдует с DDNS, поднимают свои сервера и прочее. Обычные же люди таких слов не знают и даже не представляют, что такая проблема вообще есть. Они покупают, скажем, камеру видеонаблюдения или умную розетку и не догадываются, что без какого-то сервера (чаще всего, где-то в далёком Китае) всё это работать не будет в принципе.