Датчик наличия 220В на основе Wemos D1 и о перепетиях с чатом GPT
У моего товарища дома установлен инвертор Anenji на 6,2 кВт, батарея на несколько кВт и солнечные панели. В летнее время в солнечные дни дом становится независим от городской сети. Но в пасмурные дни и осенне-весенний период энергии не хватает.
Практически весь дом работает через инвертор, включая духовку, микроволновку, электроводонагреватель, полотенцесушитель и пр. Напрямую к городской сети подключено несколько розеток на всякий случай.
Понятно, что в случае отключения сети, когда на улице пасмурно, духовкой и прочими мощными электроприборами никто не пользуется. Но отключать полотенцесушитель и нагреватель воды бывает некому. На этих и нескольких других приборах стоят WiFi розетки Tuya, но отключением дистанционно никто не занимается.
В связи с этим, понадобилось некое устройство, которое бы следило за состоянием городской сети.
Как ни странно, но поиски в инете не увенчались успехом. Очевидно китайцы считают, что отключения электроэнергии в доме невозможно, а может по какой другой причине.
Конечно, можно было попробовать использовать WiFi розетку или какой-нибудь power meter. Но при отключении энергии эти устройства не могут дать сигнал в сценарий, чтобы отключить лишние приборы, а просто сами отключаются.
Еще одной идее было купить датчик открытия окон/дверей, заменить геркон на реле, которое бы питалось от сети. Но мне не нравится идея батареек и вообще…
Совершенно неожиданно на Али попалась плата Wemos с холдером под аккумулятор 18650. Недавно был небольшой обзор.

Почему бы не попробовать использовать ее, сделав совместимой со Smart Life. Идея простая, от плюса USB порта берем сигнал и подаем через делитель на один из пинов D. При наличии напряжения на блоке питания на нем устанавливается 1, при пропадании — 0. При этом устройство остается в сети, питаясь от аккумулятора, как обещает продавец до 17 часов. В Smart Life устройство меняет статус, что позволяет сценарию управлять WiFi розетками, в частности подключенными к электронагревателям.
Копаться на форумах и в документации Tuya мне было лень, поэтому решил обратиться к «умному» чату GPT. «Без проблем» сказал он, «сваяем скетч за пять минут».
Новый «умный интеллект» сразу выдал скетч.
Предполагалось, что устройство на Wemos будет вести себя также как любое совместимое. Т.е. просто новое устройство обнаруживается приложением, указывается данные WiFi и все начинает работать. Но не тут то было. Модуль категорически не хотел обнаруживаться. Очевидно, из-за того, что Wemos не имеет на борту Bluetooth или устройства Tuya работают по какому-то своему протоколу.
Что ж, идем с претензией к ИИ.
Нужно сказать, что все время ИИ выдавал исправленные скетчи без всякого пафоса в отличие от чата Gpt набивающего себе цену.
Поиск истины занял довольно большой промежуток времени и тысячи строк кода, которые выдавал ИИ. Были предпринята куча вариантов, вплоть до пробы связать Tasmota и Tuya.
И вот наконец окончательное решение:
Попробую воспроизвести последовательность действий. Возможно что-то пропущу, тогда у вас будет возможность проконсультироваться с ИИ.
И так, для начала нужно зайти на сайт разработчиков под своим логином в Smart Life и создать новый продукт.

Выбрать нужный тип устройства, режим TuyaOS заполнить обязательные поля. В моем конкретном случае пришлось выбирать «Can't find category», ваше устройство будет кастомным.

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

Для моего проекта нужен некий switch со значением boolean и Value, в котором будет отображаться текущее напряжение аккумулятора. Важное замечание, параметр Data Transfer Type должен быть обязательно Send and Report.


Именно поэтому, я не стал выбирать из списка стандартных функций. Некоторые из них имеют свойство Report only, которое нельзя изменить. А это один из камней преткновения почему долго пришлось колдовать со скетчем. Но, в принципе, среди списка можно подобрать те, которые могут отправлять и принимать данные. Главное, чтобы они подходили под ваш проект. Также можно комбинировать стандартные и кастомные функции.
Выбираем панель, которая будет в Smart Life. Здесь можно настроить по своему усмотрению и даже загрузить свой рисунок. Получаем QRcode, который можно отсканировать в приложении и посмотреть как будет эта панель выглядеть.

Далее нужно выбрать тип устройства. И вот здесь возникла проблема: в списке нет ESP8266 или совместимого. Поэтому было предложено выбрать любое поддерживающее WiFi.

В визарде есть еще несколько шагов, но по-большому счету этого достаточно. Все остальное можно настроить позже.
Теперь необходимо добавить устройство в облачный проект. Именно через него и будет оно общаться с программой Smart Life или Tuya.
В разделе Cloud/Development создаем новый проект, если он еще не существовал.
После создания необходимо зайти в него. На странице Overview будут интересовать Access ID/Client ID и AccessSecret/Client Secret. Эти значения необходимы, чтобы получить доступ к облаку Tuya.

Теперь необходимо созданный выше девайс добавить в этот проект.
Нужно перейти на вкладку Devices, нажать Add Device и выбрать Virtual.

Из списка на вкладке My Products нужно выбрать созданный.

Откроется очередное окно с кучей вкладок с непонятными параметрами. Но нам нужна вкладка Add To Tuya App Account. Здесь будет сгенерирован QR code, который нужно отсканировать в приложении Smart Life.

После этого устройство появится на странице приложения.
На вкладке созданного виртуального устройства понадобится параметр Device ID.

Еще один подводный камень, о который споткнулись мы с ИИ. Определенные функции еще не отображаются в виртуальном устройстве. Соответственно, обратиться к ним пока невозможно.
Если зайти на вкладку Device Debugging она будет пустая.

Вверху станице написан довольно большой текст. Суть, нужно зайти на Product Details и выбрать режим DP Instruction.

Только после этого функции устройства появятся и только тогда с ними можно будет работать.

Устройства общаются с облаком Tuya посредством запросов GET/POST.
Прежде всего, нужно получить access_token. Структура примерно следующая:
GET openapi.tuyaeu.com/v1.0/token?grant_type=1
Headers: clientId, timestamp (от начала веков-1970 года) и подпись.
Подпись формируется как: clientId+«HmacSHA256»+method + "\n" + contentHash + "\n\n" + urlPath, где contentHash — некая пустая строка Hash.
Все это криптуется HmacSHA256 тем самым secret_key.
В результате получаем заветный access_token.
Чтобы отправить какие-либо данные в Smart Life используется
POST openapi.tuyaeu.com/v1.0/devices/{{deviceId}}/commands с теми же header.
В теле запроса указывается {«commands»:[{«code»:«status»,«value»:true}]} название функции, значение которой нужно передать.
Точно также формируется подпись, с включением тела запроса.
Выдан очередной довольно длинный скетч. Однако, в консоли Arduino IDE появляется ошибка: Ошибка получения токена: «request time is invalid».
Следующая ошибка: Ошибка получения токена: «code»:1004,«msg»:«sign invalid». Оказывается, что сервер Tuya требует обязательную HMAC-SHA256 подпись (sign) запроса. Еще один скетч.
Еще несколько ошибок, связанных все с тем же HMAC, то ему не хватает параметра при вызове, то он хочет, чтобы все символы были в UPPERCASE.
В итоге:
Но, это была передача только одного параметра: наличия напряжения. Следующей моей идее, было передавать текущее напряжение на аккумуляторе. И тут нас ждал очередные часы поиска истины.
Как мы с ИИ не бились, но Tuya возвращала ошибку
Ок, пересоздаем все и делаем параметр нужного свойства. Но ошибка так и не уходит. Мало того периодически появляются новые.
И наконец, вот он рабочий скетч для модуля Wemos D1!
Для того, чтобы модуль мог выходить из состояния глубокого сна, нужно GPIO16 (D0) соединить с пином Reset. Однако, ни первый ни второй не выведен на гребенку.

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

Так что, все свелось к соединению этих двух контактов.
В итоге, я выбрал одну стандартную функцию и одну кастомную для показа напряжения аккумулятора. В Smart Life это выглядит так:

Поскольку обе функции Read and Report, то в самом приложении их можно изменять. Если для первой это будет иметь значение, т.к. сработает сценарий, то изменение значения второй ничего не поменяет, потому что это значение не отправляется обратно в модуль.

Хочу сказать, что я на столько обленился, что даже очевидные ошибки, типа отсутствие нужной библиотеки при вызове функции не пробовал исправлять. Просто все ошибки, что выдавал Arduino IDE транслировались ИИ и тот тут же выдавал исправленный скетч. Кстати, любые ошибки с его стороны не только исправлялись, но и довольно подробно комментировались почему это произошло.
Но проверять и тестировать нужно тщательно. Например, в одном из предыдущих вариантов оказалось, что модуль вовсе не переходит в спящий режим. Потому что, после выполнения команды ESP.deepSleep() нужно сделать паузу, пока модуль перейдет в это самое состояние. Иначе начинает выполняться следующая команда и модуль никогда не переходит в сон. А ИИ сделал задержку до выполнения функции, а не после нее.
Время работы от аккумулятора оказалось примерно 17 часов, как и обещано на странице товара.
Ток потребления после перевода в сон составил чуть больше 9 мА.

Конечно скетч еще тестировать и тестировать. Тем не менее ИИ выдал вполне рабочий код и готов дорабатывать его по первому требованию. Например, я попросил вынести все константы в верх скетча, что было сделано практически мгновенно.
Уверен, что на изучение и перелопачивание всяких форумов по внедрению такого модуля в систему Tuya ушло бы гораздо больше сил и времени, чем пообщаться с ИИ.
Практически весь дом работает через инвертор, включая духовку, микроволновку, электроводонагреватель, полотенцесушитель и пр. Напрямую к городской сети подключено несколько розеток на всякий случай.
Понятно, что в случае отключения сети, когда на улице пасмурно, духовкой и прочими мощными электроприборами никто не пользуется. Но отключать полотенцесушитель и нагреватель воды бывает некому. На этих и нескольких других приборах стоят WiFi розетки Tuya, но отключением дистанционно никто не занимается.
В связи с этим, понадобилось некое устройство, которое бы следило за состоянием городской сети.
Как ни странно, но поиски в инете не увенчались успехом. Очевидно китайцы считают, что отключения электроэнергии в доме невозможно, а может по какой другой причине.
Конечно, можно было попробовать использовать WiFi розетку или какой-нибудь power meter. Но при отключении энергии эти устройства не могут дать сигнал в сценарий, чтобы отключить лишние приборы, а просто сами отключаются.
Еще одной идее было купить датчик открытия окон/дверей, заменить геркон на реле, которое бы питалось от сети. Но мне не нравится идея батареек и вообще…
Совершенно неожиданно на Али попалась плата Wemos с холдером под аккумулятор 18650. Недавно был небольшой обзор.

Почему бы не попробовать использовать ее, сделав совместимой со Smart Life. Идея простая, от плюса USB порта берем сигнал и подаем через делитель на один из пинов D. При наличии напряжения на блоке питания на нем устанавливается 1, при пропадании — 0. При этом устройство остается в сети, питаясь от аккумулятора, как обещает продавец до 17 часов. В Smart Life устройство меняет статус, что позволяет сценарию управлять WiFi розетками, в частности подключенными к электронагревателям.
Копаться на форумах и в документации Tuya мне было лень, поэтому решил обратиться к «умному» чату GPT. «Без проблем» сказал он, «сваяем скетч за пять минут».
И тут началось...
Прежде всего нужно создать проект на сайте девелоперов Tuya. ИИ подсказывал на какие кнопки нужно нажать и что сделать. Только вот этих самых кнопок не находилось на странице. Пришлось отправлять скриншоты. И тут чат прозрел, так у тебя новый интерфейс 2026 сайта! Странно, разве я могу использовать старый, если сайт обновился. Ну, ладно. Создал проект. Теперь нужно создать скетч. Опять же чат пообещал, что вот буквально сейчас все сделает. Только в бесплатной версии количество сообщений ограничено. А этот гад, не выдавал сразу код, а обещал это сделать быстро и без ошибок, и следующем сообщением приходилось ему говорить: Да, давай. В результате через какое-то непродолжительное время было предложено или перейти на платную версию или подождать буквально часов 12. В результате такими темпами вся эта котовасия заняла чуть ли не две недели.
Вариантов создания программы под Wemos было несколько. Вначале ИИ предложил сделать в VS Code + библиотеки. В результате выяснилось, что Tuya убрала поддержку ESP8266 из виндоуз версии. Пришлось ставить виртуальную машину Ubuntu. Но и там такой библиотеки не оказалось. Как будто не мог ИИ выяснить это сразу. Вернулись к Arduino IDE.
В созданном проекте Tuya нужно было добавить ESP8266 как новое устройство. И опять выясняется, что Tuya убрала поддержку именно этого модуля, осталась ESP32, хотя и это не гарантировано.
«Давай создадим виртуальный девайс — это займет 2 минуты!». Я разве против? Давай. И опять он забыл что у меня новый интерфейс и опять извинения, ой это моя ошибка и сейчас исправим за пять минут. И опять приходится ждать, пока в очередной раз пройдет время на использование бесплатной версии чата.
Через две недели мне все это надоело и я обратился к другому «умному» чату: Gemini. Здесь дело пошло веселее. И, хотя, частенько ИИ делал те же ошибки, что и предыдущий, первая рабочая версия скетча была готова в общей сложности часа через два.
Доведение до ума заняло еще довольно много времени. Но об этом позже.
Вариантов создания программы под Wemos было несколько. Вначале ИИ предложил сделать в VS Code + библиотеки. В результате выяснилось, что Tuya убрала поддержку ESP8266 из виндоуз версии. Пришлось ставить виртуальную машину Ubuntu. Но и там такой библиотеки не оказалось. Как будто не мог ИИ выяснить это сразу. Вернулись к Arduino IDE.
В созданном проекте Tuya нужно было добавить ESP8266 как новое устройство. И опять выясняется, что Tuya убрала поддержку именно этого модуля, осталась ESP32, хотя и это не гарантировано.
«Давай создадим виртуальный девайс — это займет 2 минуты!». Я разве против? Давай. И опять он забыл что у меня новый интерфейс и опять извинения, ой это моя ошибка и сейчас исправим за пять минут. И опять приходится ждать, пока в очередной раз пройдет время на использование бесплатной версии чата.
Через две недели мне все это надоело и я обратился к другому «умному» чату: Gemini. Здесь дело пошло веселее. И, хотя, частенько ИИ делал те же ошибки, что и предыдущий, первая рабочая версия скетча была готова в общей сложности часа через два.
Доведение до ума заняло еще довольно много времени. Но об этом позже.
Новый «умный интеллект» сразу выдал скетч.
Предполагалось, что устройство на Wemos будет вести себя также как любое совместимое. Т.е. просто новое устройство обнаруживается приложением, указывается данные WiFi и все начинает работать. Но не тут то было. Модуль категорически не хотел обнаруживаться. Очевидно, из-за того, что Wemos не имеет на борту Bluetooth или устройства Tuya работают по какому-то своему протоколу.
Что ж, идем с претензией к ИИ.
Нужно сказать, что все время ИИ выдавал исправленные скетчи без всякого пафоса в отличие от чата Gpt набивающего себе цену.
Поиск истины занял довольно большой промежуток времени и тысячи строк кода, которые выдавал ИИ. Были предпринята куча вариантов, вплоть до пробы связать Tasmota и Tuya.
И вот наконец окончательное решение:
Единственный стабильный способ заставить автономный ESP8266 работать с Tuya Cloud через код — использовать прямую авторизацию устройства по ключам (Device ID + Device Secret). В этом случае плата подключается к вашему Wi-Fi обычным кодом, а затем сразу авторизуется в вашем Cloud-проекте Tuya.
Попробую воспроизвести последовательность действий. Возможно что-то пропущу, тогда у вас будет возможность проконсультироваться с ИИ.
И так, для начала нужно зайти на сайт разработчиков под своим логином в Smart Life и создать новый продукт.

Выбрать нужный тип устройства, режим TuyaOS заполнить обязательные поля. В моем конкретном случае пришлось выбирать «Can't find category», ваше устройство будет кастомным.

Далее система перебрасывает на список стандартных функций для этого устройства. Но в моем случае ничего выбирать не нужно.
Нажав на Ок попадаем на страницу списка функций, где добавляем несколько кастомных.
Для моего проекта нужен некий switch со значением boolean и Value, в котором будет отображаться текущее напряжение аккумулятора. Важное замечание, параметр Data Transfer Type должен быть обязательно Send and Report.


Именно поэтому, я не стал выбирать из списка стандартных функций. Некоторые из них имеют свойство Report only, которое нельзя изменить. А это один из камней преткновения почему долго пришлось колдовать со скетчем. Но, в принципе, среди списка можно подобрать те, которые могут отправлять и принимать данные. Главное, чтобы они подходили под ваш проект. Также можно комбинировать стандартные и кастомные функции.
Выбираем панель, которая будет в Smart Life. Здесь можно настроить по своему усмотрению и даже загрузить свой рисунок. Получаем QRcode, который можно отсканировать в приложении и посмотреть как будет эта панель выглядеть.

Далее нужно выбрать тип устройства. И вот здесь возникла проблема: в списке нет ESP8266 или совместимого. Поэтому было предложено выбрать любое поддерживающее WiFi.

В визарде есть еще несколько шагов, но по-большому счету этого достаточно. Все остальное можно настроить позже.
Теперь необходимо добавить устройство в облачный проект. Именно через него и будет оно общаться с программой Smart Life или Tuya.
В разделе Cloud/Development создаем новый проект, если он еще не существовал.
После создания необходимо зайти в него. На странице Overview будут интересовать Access ID/Client ID и AccessSecret/Client Secret. Эти значения необходимы, чтобы получить доступ к облаку Tuya.

Теперь необходимо созданный выше девайс добавить в этот проект.
Нужно перейти на вкладку Devices, нажать Add Device и выбрать Virtual.

Из списка на вкладке My Products нужно выбрать созданный.

Откроется очередное окно с кучей вкладок с непонятными параметрами. Но нам нужна вкладка Add To Tuya App Account. Здесь будет сгенерирован QR code, который нужно отсканировать в приложении Smart Life.

После этого устройство появится на странице приложения.
На вкладке созданного виртуального устройства понадобится параметр Device ID.

Еще один подводный камень, о который споткнулись мы с ИИ. Определенные функции еще не отображаются в виртуальном устройстве. Соответственно, обратиться к ним пока невозможно.
Если зайти на вкладку Device Debugging она будет пустая.

Вверху станице написан довольно большой текст. Суть, нужно зайти на Product Details и выбрать режим DP Instruction.

Только после этого функции устройства появятся и только тогда с ними можно будет работать.

Устройства общаются с облаком Tuya посредством запросов GET/POST.
Прежде всего, нужно получить access_token. Структура примерно следующая:
GET openapi.tuyaeu.com/v1.0/token?grant_type=1
Headers: clientId, timestamp (от начала веков-1970 года) и подпись.
Подпись формируется как: clientId+«HmacSHA256»+method + "\n" + contentHash + "\n\n" + urlPath, где contentHash — некая пустая строка Hash.
Все это криптуется HmacSHA256 тем самым secret_key.
В результате получаем заветный access_token.
Чтобы отправить какие-либо данные в Smart Life используется
POST openapi.tuyaeu.com/v1.0/devices/{{deviceId}}/commands с теми же header.
В теле запроса указывается {«commands»:[{«code»:«status»,«value»:true}]} название функции, значение которой нужно передать.
Точно также формируется подпись, с включением тела запроса.
Выдан очередной довольно длинный скетч. Однако, в консоли Arduino IDE появляется ошибка: Ошибка получения токена: «request time is invalid».
Отметка времени (Timestamp), которую отправляет Wemos D1, не совпадает с реальным временем на серверах Tuya (допустимый разрыв — не более нескольких секунд).И вновь новый скетч с несколькими добавленными строчками кода.
В коде использовался параметр millis(), который выдаёт время в миллисекундах с момента включения платы (например, 1500), а сервер Tuya ждёт реальное Unix UTC время в миллисекундах (13 цифр, например 1789474030676)
Следующая ошибка: Ошибка получения токена: «code»:1004,«msg»:«sign invalid». Оказывается, что сервер Tuya требует обязательную HMAC-SHA256 подпись (sign) запроса. Еще один скетч.
Еще несколько ошибок, связанных все с тем же HMAC, то ему не хватает параметра при вызове, то он хочет, чтобы все символы были в UPPERCASE.
В итоге:
$$\text{StringToSign} = \text{client\_id} + t + \text{HTTP\_METHOD} + \text{"\n"} + \text{Content-SHA256} + \text{"\n\n"} + \text{URI}$$Но, это была передача только одного параметра: наличия напряжения. Следующей моей идее, было передавать текущее напряжение на аккумуляторе. И тут нас ждал очередные часы поиска истины.
Как мы с ИИ не бились, но Tuya возвращала ошибку
{"code":2008,"msg":"command or value not support","success":false,"t":1790002210126,"tid":"be4c60a5b5cb11f18e74b24ed3ecc0bd"}И вновь несколько часов разборки полетов. В итоге, выяснилось, что я создал параметр со свойством Report only, вместо Send and Report. На мое возражение, что мы хотим просто читать параметр, а не записывать на модуль, был ответ что так работает система через облако. Ок, пересоздаем все и делаем параметр нужного свойства. Но ошибка так и не уходит. Мало того периодически появляются новые.
{"code":1108,"msg":"uri path invalid","success":false,"t":1790001646565,"tid":"6e69ff10b5ca11f1961972feba20a1c4"}И наконец, вот он рабочий скетч для модуля Wemos D1!
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <time.h>
#include <bearssl/bearssl.h>
// ============================================================================
// БЛОК КОНФИГУРАЦИИ И КОНСТАНТ
// ============================================================================
// 1. Параметры Wi-Fi
const char* WIFI_SSID = "xxx";
const char* WIFI_PASSWORD = "12345678";
// 2. Данные Tuya Cloud API
const char* TUYA_CLIENT_ID = "eq8pu8xxxxxxxxxxxx";
const char* TUYA_CLIENT_SECRET = "dcf65c9xxxxxxxxxxxxxxxxxxxx";
const char* TUYA_DEVICE_ID = "vdevo179xxxxxxxxxxxx";
// 3. Сетевые адреса и эндпоинты HTTP
const char* TUYA_HOST = "openapi.tuyaeu.com"; // Региональный сервер (Европа)
const char* TUYA_BASE_URL = "https://openapi.tuyaeu.com";
const char* NTP_SERVER_1 = "pool.ntp.org";
const char* NTP_SERVER_2 = "time.nist.gov";
// 4. Настройка аппаратных пинов
const int PIN_SENSOR_220V = 14; // GPIO14 (Пин D5 на Wemos D1)
const int PIN_BATTERY_ADC = A0; // Вход аналогового замера
// 5. Калибровка питания и логика Deep Sleep
const float BATTERY_CALIBRATION_FACTOR = 0.00412; // Коэффициент калибровки делителя A0
const float LOW_BATTERY_THRESHOLD = 3.1; // Порог разряда для ухода в сон (В)
const uint64_t DEEP_SLEEP_DURATION_US = 15ULL * 60ULL * 1000000ULL; // Время сна (15 минут в мкс)
// 6. Тайминги и системные параметры
const unsigned long BATTERY_CHECK_INTERVAL_MS = 60000; // Интервал замеров АКБ (60 сек)
const unsigned long DEBOUNCE_DELAY_MS = 50; // Задержка антидребезга (мс)
const unsigned long SERIAL_BAUD_RATE = 115200; // Скорость Serial
const char* EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
// ============================================================================
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ СОСТОЯНИЯ
// ============================================================================
String access_token = "";
static bool last_state = HIGH;
unsigned long lastBatteryCheck = 0;
// ============================================================================
// ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
// ============================================================================
// HMAC-SHA256 (HEX в ВЕРХНЕМ регистре UPPERCASE)
String calculateHMAC(String data, String key) {
br_hmac_key_context kc;
br_hmac_context hc;
br_hmac_key_init(&kc, &br_sha256_vtable, key.c_str(), key.length());
br_hmac_init(&hc, &kc, 0);
br_hmac_update(&hc, data.c_str(), data.length());
uint8_t hmac_out[32];
br_hmac_out(&hc, hmac_out);
String hex_str = "";
for (int i = 0; i < 32; i++) {
if (hmac_out[i] < 0x10) hex_str += "0";
hex_str += String(hmac_out[i], HEX);
}
hex_str.toUpperCase();
return hex_str;
}
// SHA-256 для тела POST-запроса (в LOWERCASE)
String calculateSHA256(String data) {
br_sha256_context sc;
br_sha256_init(&sc);
br_sha256_update(&sc, data.c_str(), data.length());
uint8_t hash_out[32];
br_sha256_out(&sc, hash_out);
String hex_str = "";
for (int i = 0; i < 32; i++) {
if (hash_out[i] < 0x10) hex_str += "0";
hex_str += String(hash_out[i], HEX);
}
hex_str.toLowerCase();
return hex_str;
}
uint64_t getEpochMillis() {
time_t now = time(nullptr);
return ((uint64_t)now * 1000ULL);
}
// Безопасная функция ухода в глубокий сон
void goToSleep(uint64_t sleepTimeUs) {
Serial.println("-> Отключаем Wi-Fi и уходим в Deep Sleep...");
Serial.flush(); // Ждем полной отправки данных по Serial
// 1. Принудительно обесточиваем Wi-Fi радиомодуль
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
WiFi.forceSleepBegin();
delay(100); // Задержка для завершения аппаратного отключения радиомодуля
// 2. Переводим чип в глубокий сон
ESP.deepSleep(sleepTimeUs, WAKE_RF_DEFAULT);
// 3. Задержка ПОСЛЕ deepSleep блокирует поток до аппаратного отключения
delay(1000);
}
// Запрос авторизационного токена Tuya
void getTuyaToken() {
if (WiFi.status() != WL_CONNECTED) return;
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
String uri = "/v1.0/token?grant_type=1";
String url = String(TUYA_BASE_URL) + uri;
uint64_t t_now = getEpochMillis();
String t_str = String(t_now);
String strToSign = String(TUYA_CLIENT_ID) + t_str + "GET\n" + String(EMPTY_BODY_SHA256) + "\n\n" + uri;
String sign = calculateHMAC(strToSign, TUYA_CLIENT_SECRET);
http.begin(client, url);
http.addHeader("client_id", TUYA_CLIENT_ID);
http.addHeader("sign", sign);
http.addHeader("t", t_str);
http.addHeader("sign_method", "HMAC-SHA256");
int httpCode = http.GET();
if (httpCode > 0) {
String payload = http.getString();
DynamicJsonDocument doc(1024);
deserializeJson(doc, payload);
if (doc["success"]) {
access_token = doc["result"]["access_token"].as<String>();
Serial.println("-> УСПЕХ: Токен Tuya успешно получен!");
} else {
Serial.print("-> Ошибка получения токена: ");
Serial.println(payload);
}
} else {
Serial.print("-> Ошибка HTTP запроса: ");
Serial.println(http.errorToString(httpCode));
}
http.end();
}
// Универсальная функция отправки payloads в Tuya
void sendTuyaPayload(String jsonBody, bool isRetry = false) {
if (access_token == "") {
getTuyaToken();
}
if (access_token == "") {
Serial.println("-> Ошибка: Нет действительного токена доступа!");
return;
}
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
String uri = "/v1.0/devices/" + String(TUYA_DEVICE_ID) + "/commands";
String url = String(TUYA_BASE_URL) + uri;
uint64_t t_now = getEpochMillis();
String t_str = String(t_now);
String bodyHash = calculateSHA256(jsonBody);
String strToSign = String(TUYA_CLIENT_ID) + access_token + t_str + "POST\n" + bodyHash + "\n\n" + uri;
String sign = calculateHMAC(strToSign, TUYA_CLIENT_SECRET);
http.begin(client, url);
http.addHeader("client_id", TUYA_CLIENT_ID);
http.addHeader("access_token", access_token);
http.addHeader("sign", sign);
http.addHeader("t", t_str);
http.addHeader("sign_method", "HMAC-SHA256");
http.addHeader("Content-Type", "application/json");
int httpCode = http.POST(jsonBody);
if (httpCode > 0) {
String payload = http.getString();
Serial.print("-> Ответ сервера Tuya: ");
Serial.println(payload);
DynamicJsonDocument doc(1024);
DeserializationError err = deserializeJson(doc, payload);
if (!err) {
if (doc["success"]) {
Serial.println("-> УСПЕХ: Команда доставлена!");
} else {
int errCode = doc["code"];
// Проверка истечения токена (код 1010)
if (errCode == 1010 || payload.indexOf("token invalid") != -1) {
if (!isRetry) {
Serial.println("-> ВНИМАНИЕ [1010]: Токен устарел. Запрашиваем новый...");
access_token = "";
getTuyaToken();
if (access_token != "") {
Serial.println("-> Повторная отправка команды с новым токеном...");
http.end();
sendTuyaPayload(jsonBody, true);
return;
}
} else {
Serial.println("-> Ошибка: Повторная отправка не удалась.");
}
}
}
}
} else {
Serial.print("-> Ошибка отправки: ");
Serial.println(http.errorToString(httpCode));
}
http.end();
}
// Замер напряжения аккумулятора
float getBatteryVoltage() {
int rawADC = analogRead(PIN_BATTERY_ADC);
float voltage = rawADC * BATTERY_CALIBRATION_FACTOR;
return voltage;
}
// Замер батареи и обработка логики сна
void processBatteryTelemetry() {
float vBatt = getBatteryVoltage();
Serial.print("Считано с A0: ");
Serial.print(vBatt);
Serial.println(" В");
int tuyaValue = (int)(vBatt * 100);
String payload = "{\"commands\":[{\"code\":\"voltage_current\",\"value\":" + String(tuyaValue) + "}]}";
sendTuyaPayload(payload);
bool is220Present = (digitalRead(PIN_SENSOR_220V) == LOW);
if (vBatt < LOW_BATTERY_THRESHOLD) {
if (is220Present) {
Serial.println("-> Батарея разряжена, но 220V ЕСТЬ. Работаем и заряжаемся.");
} else {
Serial.println("-> КРИТИЧЕСКИЙ ЗАРЯД и 220V НЕТ! Вызываем Deep Sleep...");
goToSleep(DEEP_SLEEP_DURATION_US);
}
}
}
// ============================================================================
// SETUP И LOOP
// ============================================================================
void setup() {
Serial.begin(SERIAL_BAUD_RATE);
pinMode(PIN_SENSOR_220V, INPUT_PULLUP);
Serial.println("\nПодключение к Wi-Fi...");
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWi-Fi успешно подключен!");
configTime(0, 0, NTP_SERVER_1, NTP_SERVER_2);
Serial.print("Синхронизация времени");
time_t now = time(nullptr);
while (now < 100000) {
delay(500);
Serial.print(".");
now = time(nullptr);
}
Serial.println("\nВремя синхронизировано!");
getTuyaToken();
// Инициализация состояний при старте/пробуждении
last_state = digitalRead(PIN_SENSOR_220V);
Serial.print("Текущее состояние 220V при старте: ");
Serial.println(last_state == LOW ? "ЕСТЬ" : "НЕТ");
String statePayload = "{\"commands\":[{\"code\":\"charge_switch\",\"value\":" + String(last_state == LOW ? "true" : "false") + "}]}";
sendTuyaPayload(statePayload);
processBatteryTelemetry();
lastBatteryCheck = millis();
}
void loop() {
// 1. Периодический замер напряжения аккумулятора
unsigned long currentMillis = millis();
if (currentMillis - lastBatteryCheck >= BATTERY_CHECK_INTERVAL_MS) {
lastBatteryCheck = currentMillis;
processBatteryTelemetry();
}
// 2. Отслеживание изменения уровня 220V
bool current_state = digitalRead(PIN_SENSOR_220V);
if (current_state != last_state) {
delay(DEBOUNCE_DELAY_MS);
if (digitalRead(PIN_SENSOR_220V) == current_state) {
last_state = current_state;
Serial.print("Изменение состояния 220V: ");
Serial.println(last_state == LOW ? "ЕСТЬ (LOW)" : "НЕТ (HIGH)");
String statePayload = "{\"commands\":[{\"code\":\"charge_switch\",\"value\":" + String(last_state == LOW ? "true" : "false") + "}]}";
sendTuyaPayload(statePayload);
if (last_state == HIGH) {
processBatteryTelemetry();
}
}
}
}
Для того, чтобы модуль мог выходить из состояния глубокого сна, нужно GPIO16 (D0) соединить с пином Reset. Однако, ни первый ни второй не выведен на гребенку.

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

Так что, все свелось к соединению этих двух контактов.
В итоге, я выбрал одну стандартную функцию и одну кастомную для показа напряжения аккумулятора. В Smart Life это выглядит так:

Поскольку обе функции Read and Report, то в самом приложении их можно изменять. Если для первой это будет иметь значение, т.к. сработает сценарий, то изменение значения второй ничего не поменяет, потому что это значение не отправляется обратно в модуль.

Хочу сказать, что я на столько обленился, что даже очевидные ошибки, типа отсутствие нужной библиотеки при вызове функции не пробовал исправлять. Просто все ошибки, что выдавал Arduino IDE транслировались ИИ и тот тут же выдавал исправленный скетч. Кстати, любые ошибки с его стороны не только исправлялись, но и довольно подробно комментировались почему это произошло.
Но проверять и тестировать нужно тщательно. Например, в одном из предыдущих вариантов оказалось, что модуль вовсе не переходит в спящий режим. Потому что, после выполнения команды ESP.deepSleep() нужно сделать паузу, пока модуль перейдет в это самое состояние. Иначе начинает выполняться следующая команда и модуль никогда не переходит в сон. А ИИ сделал задержку до выполнения функции, а не после нее.
Время работы от аккумулятора оказалось примерно 17 часов, как и обещано на странице товара.
Ток потребления после перевода в сон составил чуть больше 9 мА.

Конечно скетч еще тестировать и тестировать. Тем не менее ИИ выдал вполне рабочий код и готов дорабатывать его по первому требованию. Например, я попросил вынести все константы в верх скетча, что было сделано практически мгновенно.
Уверен, что на изучение и перелопачивание всяких форумов по внедрению такого модуля в систему Tuya ушло бы гораздо больше сил и времени, чем пообщаться с ИИ.
Самые обсуждаемые обзоры
Я автоматизации и карточки для HA так делаю с помощью бесплатного Gemini в поиске Google. Причём, он вполне способен сохранять контекст в течение для (и это примерно сотня страниц диалога). И конфиги для ESPHome тоже делал.
Да, порою нужны десятки итераций, нужно следить, чтобы самостоятельно не «ушёл налево» (не поменял метод достижения цели), отсеивать совсем уж очевидные ошибки… Но вариант «поставил задачу — скопировал код — заскриншотил ошибку/результат — скормил ИИ — скопировал код...» работает.
Ну или перейти с облачного smart life на православный Home Assistant, там возможно писать автоматизацию, основываясь на внезапной недоступности наблюдаемого устройства, так что умная розетка в городской сети была бы отличным индикатором и триггером для дальнейших действий.
Предлагаемый вариант — запитать «прибор» от того же, чем питается роутер. При просыпании раз в пять минут, мониторинге, репорте и засыпании это практически не увеличит расход от этого резерва, однако из схемы уйдёт лишний аккумулятор со своими проблемами — заряд, разряд, потеря ёмкости, вздутие… Далее не мерять присутствие напряжение в розетке, когда по факту инвертор уже фактически весь дом отключил, а мерять напряжение на АКБ, от которых инвертор дом питает. Это позволит не бороться с последствиями (дом обесточен полностью), а по мере разряда превентивно отключать мощные потребители, оставляя хоть что-то в доме работающем до последнего электрона в аккумуляторе.