Показания счётчиков сдаёт робот: headless-браузер вместо меня раз в месяц

Каждый месяц календарь напоминал мне передать показания счётчиков и даже присылал готовые цифры: дом их и так знает. Оставалось самое скучное: открыть сайт, залогиниться и вбить пять чисел руками. Надоело! Теперь их вбивает робот. Идём под кат…

Как было

Воду считают два самодельных ESP32: ловят импульсы с водяных счётчиков на кухне и в санузле и отдают показания в Home Assistant по MQTT. Электричество HA тоже знает. Раз в месяц срабатывало событие в календаре, и HA присылал мне и жене в телеграм напоминалку с цифрами. Дальше руками: личный кабинет, форма водоканала, четыре числа, форма энергосбыта, ещё одно. Минут десять скуки, двенадцать раз в год.

Почему браузер

Первая мысль была стандартная: посмотреть в DevTools, какой запрос уходит при отправке, и повторять его скриптом. Не вышло. Личный кабинет написан на Vaadin Flow, интерфейс у него живёт на сервере, а в запросах летят внутренние идентификаторы компонентов, которые сервер выдаёт на одну сессию. Подделать такой запрос нельзя, поэтому на сайт ходит настоящий браузер: headless Chromium через Playwright.

Схема передачи показаний: самодельные ESP32 снимают импульсы со счётчиков воды, показания попадают в Home Assistant, оттуда по событию календаря скрипт вызывает мост в отдельном LXC-контейнере, мост через Playwright и headless Chromium заполняет формы водоканала и энергосбыта в личном кабинете; ниже последовательность одной отправки: сухой прогон, отметка месяца, боевая отправка, уведомление
Путь показания: от импульса на счётчике до формы в личном кабинете

Отдельный контейнер

Chromium с Playwright тянет сотни мегабайт, и пихать это в Home Assistant мне не хотелось. Мост живёт в своём LXC на Proxmox: гигабайт диска, полгига памяти, одно ядро. Внутри маленькое приложение на FastAPI:

GET  /health    без ключа — жив ли сервис и в каком режиме
GET  /meters    какие приборы он знает
GET  /inspect   зайти на сайт и показать, что нашлось
                (ничего не вводит — разведка)
POST /send      ввести показания

X-API-Key: <токен>   сверяется через hmac.compare_digest

Порт слушается только в локалке, ключ проверяется в каждом запросе. /inspect удобен для проверки: заходит на сайт и рассказывает, что видит, ничего не вводя.

Предохранители

Если робот в умном доме ошибётся, включится не тот свет. А тут ошибка превращается в квитанцию: передал лишний ноль и объясняйся потом с поставщиком. Поэтому проверок в мосте много:

  • дробные показания округляются только вниз, больше, чем накручено на счётчике, не передать;
  • новое показание не может быть меньше того, что уже стоит на сайте;
  • прирост за месяц ограничен сверху, всё, что выше, считается сбоем датчика;
  • если прошлое показание на странице не читается, мост останавливается;
  • сухой режим включается в конфиге на сервере, запросом его не выключить.

Сама отправка идёт в три шага. Сначала сухой прогон: мост логинится, находит приборы, доходит до форм и ничего не шлёт. Если всё на месте, ставится отметка «в этом месяце отправляли», и только потом цифры уходят по-настоящему. Отметка стоит до отправки специально: если сайт показания принял, а ответ потерялся, второй раз они не уйдут. Повторов при ошибке тоже нет, вместо них приходит сообщение «проверь на сайте».

Итог

Теперь в нужный день календарь дёргает скрипт, мост проходит обе формы, и в телеграм прилетает «показания переданы». В сентябре показания впервые ушли без меня)