В сети сотни статей про использование ESP32 для съема показаний приборов учета, но практически все из них про интеграцию с «умным домом». Мой вариант реализации решает практическую задачу - просто сам передает показания, снимая с меня эту задачу.

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

Сам проект опубликован здесь.

Важное замечание про WiFi!!! Для подключения ESP32 обязательно сделайте отдельный выделенный SSID. Иначе, трафик ваших домашних устройств будет сильно загружать  ESP32.

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();

Этот запрос возвращает такие объекты:

Скрытый текст
{
    "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();

Этот запрос вернет нам такие объекты:

Скрытый текст
{
    "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. Планирую воспользоваться советами, которые мне уже дали в комментариях, и сделать запись в циклический буфер.