В сети сотни статей про использование ESP32 для съема показаний приборов учета, но практически все из них про интеграцию с «умным домом». Мой вариант реализации решает практическую задачу - просто сам передает показания, снимая с меня эту задачу.
Основное отличие моего решения от тех, что я видел, заключается в максимальной автономности и легкости контроля и управления. ESP32 самостоятельно общается с сайтом, который принимает показания приборов учета посредством встроенного http клиента. Процесс контролируется и управляется встроенным в ESP32 telegram ботом.
API сайта принимающего показания
Решать эту задачу я начал с изучения API тех сайтов, которые я использовал для передачи показаний. В моем случае это были сайты https://www.mosenergosbyt.ru/ и https://мособлеирц.рф/. Исследовал API банально, используя F12 в своем браузере. Запускаем режим отладки страницы, включаем запись, аутентифицируемся, передаем показания и исследуем методы POST, которые обычно передают и принимают в качестве ответов объекты json.
Сразу скажу, что ГИС ЖКХ отбросил сразу из-за сложности аутентификации, привязки к госуслугам и 2FA. Однако, если там есть система авторизации по токенам и кто-то знает как ей пользоваться, то было бы интересно перенять такой опыт.
API сайта мосэнергосбыта мне не понравился: там используются плагины, и для разных городов и улиц АPI запросы могут существенно различаться, делая публикацию результатов работы практически бесполезной и не интересной. А вот сайт мособлеирц имеет достаточно понятный и унифицированный API.
Для исследования API сайта я написал python скрипт. Он лежит в проекте. С его помощь Вы можете исследовать свой личный кабинет в мособлеирц или использовать как отправную точку для исследования API своего поставщика.
Для аутентификации на сайте нужно методом POST отправить JSON объект со своими учетными данными
http.begin(client, "https://lkk.mosobleirc.ru/api/tenants-registration/v2/login");
http.addHeader("Content-Type", "application/json");
StaticJsonDocument<512> req;
req["phone"] = this->user;
req["password"] = this->pass;
req["loginMethod"] = "PERSONAL_OFFICE";
serializeJson(req, json);
log_i("req memory usage %d", req.memoryUsage() );
// getting the access token
statusCode = http.POST( json );
http.end();
if ( this->checkForErrors(statusCode) ) {
client.stop();
return 0;
};
Если аутентификация прошла успешно, то нам в ответе пришлют токен, который далее нужно подставлять в атрибут X-Auth-Tenant-Token в заголовки всех последующих запросов.
Здесь я использую StaticJsonDocument фиксированного размера, который подобрал экспериментальным путем. Замечу, что этот объект создается в куче. Также в проекте я использую DynamicJsonDocument, которые размещаются в стеке. Если размер выбрать меньше необходимого, то возникнет исключительная ситуация, и ESP32 будет перезагружаться.
Далее методом GET нам нужно запросить список своих "домов". Это объекты, в которых указаны почтовые адреса и какие счетчики по этим адресам установлены. У меня в личном кабинете зарегистрировано несколько адресов, поэтому мне нужно искать объект с нужными мне счетчиками.
DynamicJsonDocument eircItems(512);
String _url = "https://lkk.mosobleirc.ru/api/api/clients/configuration-items";
http.begin(client, _url );
http.addHeader("X-Auth-Tenant-Token", this->token);
http.addHeader("Content-Type", "application/json");
log_i( "GET url : %s ", _url.c_str() );
statusCode = http.GET();
Этот запрос возвращает такие объекты:
Hidden text
{
"id": 2000000,
"name": "Квартира",
"isBasic": false,
"isOwner": false,
"isRenter": true,
"address": {
"id": 2000000,
"city": "годод",
"street": "улица",
"house": "д.1",
"number": "кв.1",
"room": "кв.1",
"location": " город, улица, д.1, кв.1",
"shortLocation": "улица, д.1"
},
"personalAccount": {
"id": 1300000,
"systemId": null,
"number": "28984398",
"utilitiesBalance": 0.00,
"repairsBalance": 0,
"paymentInsurance": 0,
"receiveReceiptByMail": true,
"receiptsEmail": null,
"newNotConfirmedReceiptsEmail": null
}
}
Здесь нам интересны id и name. На Id будут ссылаться счетчики, а name будет использовать telegram бот. Из любопытного здесь еще есть баланс в поле utilitiesBalance, но нам он в этой задаче не интересен.
Теперь можно запросить список счетчиков для id того дома, который мы выбрали:
unsigned int iid = v["id"].as<unsigned int>();
String itemName = v["name"].as<const char*>();
DynamicJsonDocument eircmeasures(2048);
String _url = "https://lkk.mosobleirc.ru/api/api/clients/meters/for-item/";
_url += String(iid);
http.begin(client, _url);
http.addHeader("X-Auth-Tenant-Token", this->token);
http.addHeader("Content-Type", "application/json");
log_i( "GET url : %s ", _url.c_str() );
statusCode = http.GET();
Этот запрос вернет нам такие объекты:
Hidden text
{
"meter": {
"id": 6180000,
"name": "2938042",
"type": "HotWater",
"number": "2938042",
"configurationItemId": 2000000,
"capacity": "99999.999",
"unit": "METERS",
"attorneyDeadline": "2026-01-01",
"tariffCount": 1,
"isAttorney": true,
"isSystem": true,
"lastValue": {
"id": 420200980,
"total": {
"value": 142,
"displayValue": "142",
"consumptionValue": 3,
"displayConsumptionValue": "+3.0",
"minValue": 142.0
},
"tariff1": {
"value": 142,
"displayValue": "142",
"consumptionValue": 3,
"displayConsumptionValue": "+3.0",
"minValue": 142.0
},
"tariff2": null,
"tariff3": null,
"sender": {
"userId": 4,
"firstName": "клиентом ",
"lastName": "Передано",
"middleName": null,
"isTenant": false
},
"receivedDate": "2023-08-10T09:00:00Z",
"status": "PROCESSED",
"settlementPeriod": {
"year": 2023,
"month": 8
},
"isAverageValue": false
}
},
"valueSendInfo": {
"meterIndicationDate": {
"from": 14,
"to": 19,
"fromDate": "14-09-2023",
"toDate": "19-09-2023"
},
"willBeCountForPeriod": {
"year": 2023,
"month": 8
},
"notes": [
{
"type": "Field",
"message": "Достаточно ввести цифры до запятой. Цифры после запятой вспомогательные и не требуют внесения."
}
],
"isAbleToSendMeterValues": false,
"isInAllowedPeriod": false,
"isValueSentForCurrentPeriod": true,
"isAutomaticModeOnly": false,
"isFractionalPartRequired": false
}
}
Здесь и текущие показания по тарифам (в случае нашего аналогового счетчика, это единый тариф), и дата поверки attorneyDeadline, и период, когда данные счетчиков принимаются valueSendInfo - из этих данных ESP32 узнает, когда нужно передавать показания за текущий месяц.
Когда наступит время передавать показания, ESP отправит такой запрос:
String _url = "https://lkk.mosobleirc.ru/api/api/clients/meters/";
_url += String(measureId);
_url += String("/values?withOptionalCheck=true");
http.begin(client, _url);
http.addHeader("X-Auth-Tenant-Token", this->token);
http.addHeader("Content-Type", "application/json");
DynamicJsonDocument payload(128);
payload["value1"] = newVal;
json = "";
serializeJson(payload, json);
statusCode = http.POST( json );
Кстати о времени: время берется по протоколу NTP, хотя также можно брать и из telegram бота, так как там в каждом запросе передается время UTC.
Бот
Контролировать процесс было решено с помощью telegram бота, встроенного в сам ESP32. Бот сделан на базе библиотеки https://github.com/GyverLibs/FastBot. Бот позволяет задать начальные значения отсчета, корректировать их по необходимости, посмотреть текущее состояние, контролировать процесс передачи показаний и заодно напомнит о дате очередной поверки счетчиков.
Команд у бота не много:
help - выдать список команд
get - сходить на сайт мособлеирц, забрать оттуда текущие "тамошние" данные и показать все вместе с текущими значениями счетчиков
val - то же, что и get, но не ходить на сайт, а показать последние считанные оттуда данные
push - принудительная отправка текущих показаний
set - первичная установка и/или корректировка текущих значений счетчиков
Формат команды такой:
set <hot> <col> [<data>]
Здесь hot и col - это целые числа в десятках литров.
Например, если прибор сейчас показывает значение 00012,345 (345 красные цифры единиц литров), нам нужно отбросить последнюю красную цифру и запятую. Таким образом, нужно передать боту только 1234. При этом, в мособлеирц бот отправит только количество целых кубометров, т.е. 12.
Пример.
Мои текущие показания:
Горячая вода - 00145,765
Холодная вода - 00305,236
отбрасываем запятые и по одной последней цифре!!! Получается, что боту нужно отправить такую команду
/set 14576 30523
Устройство счетчиков
Физические счетчики воды (приборы учета), конечно, должны быть оснащены герконами, импульсы которых считывает контроллер.
Программные счетчики импульсов пробовал делать и на основе прерываний, и с аппаратной, и с программной защитой от дребезга. Остановился на программной защите, основанной на таймерах. Оказалось, это самый практичный и надежный вариант. Тестовая эксплуатация на протяжении двух месяцев показала практически 100% точность подсчета импульсов. Такой вариант не требует никакой обвязки, провода от счетчиков можно подключить непосредственно к пинам контроллера. Отсутствие обвязки значительно упрощает проект и делает его более доступным для повторения, причем незначительно влияет на качество работы.
Подсчитанные значения сохраняются в энергонезависимой памяти контроллера, чтобы можно было пережить пропадания питания. Я использовал библиотеку preferences, но позже внимательно на нее посмотрев, решил, что в перспективе переделаю эту часть для повышения срока службы flash памяти.
Дело в том, что в ESP32 eeprom эмулируется в одной из страниц flash памяти. Подробнее об этом можно прочитать здесь https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/storage/nvs_flash.html
При этом, если я правильно понял, сами значения библиотекой preferences пишутся оптимально, когда каждое обновление счетчиков занимает новый 32 байтный блок, но вот в начале страницы заголовок обновляется с каждой записью. Это значит, что ресурс этого сегмента памяти иссякнет быстрее всего. Тут не стоит сильно переживать. Объявленный ресурс 100 000 циклов, и этого должно хватить на 1000 кубометров, но все равно небольшая неудовлетворенность присутствует :).
В ближайшее время попробую доработать проект, чтобы увеличить ресурс flash. Планирую воспользоваться советами, которые мне уже дали в комментариях, и сделать запись в циклический буфер.