Каждый месяц у меня была одна и та же задача:
снять и передать показания воды;
снять и передать показания электроэнергии;
проверить начисления;
найти ссылки на оплату;
оплатить счета.
Сам процесс занимал немного времени, но требовал внимания. Стоило забыть про очередное 21 число, как начинались перерасчёты, начисления по среднему и прочие коммунальные приключения.
Поэтому я решил автоматизировать всё полностью. В результате сейчас:
-
Home Assistant собирает показания всех счётчиков;
-
вода распознаётся AI-моделью на ESP32-CAM;
-
электричество учитывается по импульсам светодиода счётчика;
-
показания автоматически отправляются поставщикам услуг;
-
начисления автоматически проверяются;
-
ссылки на оплату автоматически собираются;
-
Telegram присылает готовый отчёт.
Моё участие в процессе сведено практически к нулю.
Электросчётчик
С электросчётчиком всё оказалось достаточно просто. На корпусе есть импульсный светодиод с маркировкой: 3200 имп/кВт·ч. Каждая вспышка соответствует определённому количеству потреблённой энергии.
Подключаться к внутренним интерфейсам счётчика и тем более лезть под пломбы мне не хотелось, поэтому был выбран полностью бесконтактный вариант.
Используемые компоненты:
-
Wemos D1 Mini;
-
датчик освещённости TEMT6000;
-
ESPHome.
Датчик установлен напротив светодиода счётчика и фиксирует каждую вспышку. После получения импульса Home Assistant увеличивает значение энергии на: 1 / 3200 кВт·ч. Выглядит это как то так, плату Wemos d1 mini, нужно установить снаружи эл. щитка особенно если он металлический.
Отдельно ведётся учёт дневного и ночного тарифа. В результате Home Assistant знает:
-
День;
-
Ночь;
-
Общий расход;
-
Историю потребления.
-
Так же на лету можно корректировать показания день\ночь если произошел рассинхрон
Так это выглядит в HA

Настройка интеграции

Скетч ESPHome
esphome: name: electricmeter friendly_name: ElectricMeter on_boot: priority: -10 then: - lambda: |- id(flash_counter_day_sensor).publish_state((float) id(flash_day)); id(flash_counter_night_sensor).publish_state((float) id(flash_night)); id(energy_day_sensor).publish_state(id(energy_day_total)); id(energy_night_sensor).publish_state(id(energy_night_total)); id(energy_sensor).publish_state(id(energy_day_total) + id(energy_night_total));esp8266: board: d1_mini restore_from_flash: truepreferences: flash_write_interval: 1min#logger:api: encryption: key: ""ota: - platform: esphome password: ""wifi: ssid: !secret wifi_ssid password: !secret wifi_password ap: ssid: "" password: ""captive_portal:globals: - id: flash_day type: int restore_value: yes initial_value: '0' - id: flash_night type: int restore_value: yes initial_value: '0' - id: energy_day_total type: float restore_value: yes initial_value: '2893.30000' # 9258560 / 3200 - id: energy_night_total type: float restore_value: yes initial_value: '1094.30000' # 3501760 / 3200time: - platform: homeassistant id: esptime timezone: Europe/Amsterdam on_time: - seconds: 0 minutes: 0 hours: 0 then: - lambda: |- id(flash_day) = 0; id(flash_night) = 0; id(flash_counter_day_sensor).publish_state(0); id(flash_counter_night_sensor).publish_state(0);sensor: - platform: adc pin: A0 name: "TEMT6000 raw" id: light_raw update_interval: 10ms - platform: template name: "Освещенность в люксах" id: light_lux unit_of_measurement: "lx" accuracy_decimals: 1 lambda: |- return id(light_raw).state * 1000.0f; update_interval: 10ms - platform: template name: "Импульсы день" id: flash_counter_day_sensor unit_of_measurement: "imp" accuracy_decimals: 0 lambda: |- return (float) id(flash_day); - platform: template name: "Импульсы ночь1" id: flash_counter_night_sensor unit_of_measurement: "imp" accuracy_decimals: 0 lambda: |- return (float) id(flash_night); - platform: template name: "Энергия день" id: energy_day_sensor unit_of_measurement: "kWh" device_class: energy state_class: total_increasing accuracy_decimals: 5 lambda: |- return id(energy_day_total); - platform: template name: "Энергия ночь1" id: energy_night_sensor unit_of_measurement: "kWh" device_class: energy state_class: total_increasing accuracy_decimals: 5 lambda: |- return id(energy_night_total); - platform: template name: "Энергия всего1 " id: energy_sensor unit_of_measurement: "kWh" device_class: energy state_class: total_increasing accuracy_decimals: 5 lambda: |- return id(energy_day_total) + id(energy_night_total);number: - platform: template name: "Правка энергия день" id: edit_energy_day min_value: 0 max_value: 100000 step: 0.00001 mode: box optimistic: false lambda: |- return id(energy_day_total); set_action: - lambda: |- id(energy_day_total) = x; id(energy_day_sensor).publish_state(id(energy_day_total)); id(energy_sensor).publish_state(id(energy_day_total) + id(energy_night_total)); - platform: template name: "Правка энергия ночь1" id: edit_energy_night min_value: 0 max_value: 100000 step: 0.00001 mode: box optimistic: false lambda: |- return id(energy_night_total); set_action: - lambda: |- id(energy_night_total) = x; id(energy_night_sensor).publish_state(id(energy_night_total)); id(energy_sensor).publish_state(id(energy_day_total) + id(energy_night_total));interval: - interval: 10ms then: - lambda: |- static float last_lux = 0.0f; static uint32_t last_flash_time = 0; float current_lux = id(light_lux).state; uint32_t now_ms = millis(); if ((current_lux - last_lux) > 150.0f && (now_ms - last_flash_time > 150)) { last_flash_time = now_ms; auto now_time = id(esptime).now(); bool is_day = true; if (now_time.is_valid()) { is_day = (now_time.hour >= 7 && now_time.hour < 23); } if (is_day) { id(flash_day)++; id(energy_day_total) += 1.0f / 3200.0f; id(flash_counter_day_sensor).publish_state(id(flash_day)); id(energy_day_sensor).publish_state(id(energy_day_total)); } else { id(flash_night)++; id(energy_night_total) += 1.0f / 3200.0f; id(flash_counter_night_sensor).publish_state(id(flash_night)); id(energy_night_sensor).publish_state(id(energy_night_total)); } id(energy_sensor).publish_state(id(energy_day_total) + id(energy_night_total)); } last_lux = current_lux;
BASH скрипт отправки показаний эл. энергии
#!/usr/bin/env bashset -euo pipefail################################################### --- НАСТРОЙКИ ---##################################################RKS_EMAIL="Логин"RKS_PASSWORD="Пароль"ACCOUNT_ID="айди аккаута"DEVICE_ID="айди прибора учета"HA_URL="http://127.0.0.1:8123"HA_TOKEN="" # токен HATG_BOT="токен бота ТГ"TG_CHATS=("айди пользователей" "айди пользователей")TARIFF_DAY="ab89b37f-94e4-11ea-960f-00155d016301"TARIFF_NIGHT="ab89b380-94e4-11ea-960f-00155d016301"################################################### --- ФУНКЦИИ ---##################################################send_telegram() { local TEXT="$1" for CHAT in "${TG_CHATS[@]}"; do curl -s -X POST "https://api.telegram.org/bot${TG_BOT}/sendMessage" \ -d chat_id="$CHAT" \ -d text="$TEXT" >/dev/null done}fail() { send_telegram "❌ Ошибка РКС:\n$1" exit 1}################################################### --- 1. Получаем токен ---##################################################TOKEN=$(curl -k -s -X POST "https://lk.rks-energo.ru/api/signin" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -H "User-Agent: Mozilla/5.0" \ -H "Origin: https://lk.rks-energo.ru" \ -H "Referer: https://lk.rks-energo.ru/" \ -d "{\"email\":\"$RKS_EMAIL\",\"password\":\"$RKS_PASSWORD\"}" \ | jq -r '.token')[[ -z "$TOKEN" || "$TOKEN" == "null" ]] && fail "Не удалось получить токен"################################################### --- 2. Получаем баланс ---##################################################RESPONSE=$(curl -k -s -X GET "https://lk.rks-energo.ru/api/personalAccount" \ -H "Authorization: Bearer $TOKEN" \ -H "Accept: application/json")TOTAL=$(echo "$RESPONSE" | jq -r '.data[0].balance')VALUE_DAY=$(echo "$RESPONSE" | jq -r '.data[0].balance_details["1"].Value // 0' | tr ',' '.')VALUE_NIGHT=$(echo "$RESPONSE" | jq -r '.data[0].balance_details["2"].Value // 0' | tr ',' '.')################################################### --- 3. Получаем ссылку на оплату ---##################################################AMOUNT=$(printf "%.2f" "$TOTAL")ORDER_RESPONSE=$(curl -k -s -X GET \ "https://lk.rks-energo.ru/api/acquiring/registerOrder?personal_account_id=$ACCOUNT_ID&amount=$AMOUNT" \ -H "Authorization: Bearer $TOKEN" \ -H "Accept: application/json")PAY_URL=$(echo "$ORDER_RESPONSE" | jq -r '.data.link // empty')################################################### --- 4. Обновляем сенсоры HA ---##################################################ha_post() { local ENTITY="$1" local STATE="$2" local NAME="$3" curl -s -X POST "$HA_URL/api/states/$ENTITY" \ -H "Authorization: Bearer $HA_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"state\":\"$STATE\",\"attributes\":{\"unit_of_measurement\":\"₽\",\"friendly_name\":\"$NAME\"}}" >/dev/null}ha_post "sensor.rks_penia" "$VALUE_NIGHT" "РКС Баланс Пеня"ha_post "sensor.rks_current" "$VALUE_DAY" "РКС Баланс Текущий"ha_post "sensor.rks_total" "$TOTAL" "РКС Баланс Общий"if [ -n "$PAY_URL" ]; then ha_post "sensor.rks_payment_url" "$PAY_URL" "РКС Ссылка на оплату"fi################################################### --- 5. Берём показания с HA ---##################################################VALUE_DAY=$(curl -s -X GET "$HA_URL/api/states/sensor.electricmeter_energiia_den" \ -H "Authorization: Bearer $HA_TOKEN" | jq -r '.state')VALUE_DAY=$(printf "%.2f" "$VALUE_DAY")VALUE_NIGHT=$(curl -s -X GET "$HA_URL/api/states/sensor.electricmeter_energiia_noch1" \ -H "Authorization: Bearer $HA_TOKEN" | jq -r '.state')VALUE_NIGHT=$(printf "%.2f" "$VALUE_NIGHT")[[ ! "$VALUE_DAY" =~ ^[0-9]+([.][0-9]+)?$ ]] && fail "Дневное значение не число: $VALUE_DAY"[[ ! "$VALUE_NIGHT" =~ ^[0-9]+([.][0-9]+)?$ ]] && fail "Ночное значение не число: $VALUE_NIGHT"################################################### --- 6. Формируем values для POST ---##################################################VALUES="[{\"value\":$VALUE_DAY,\"tariff_zone_id\":\"$TARIFF_DAY\"},{\"value\":$VALUE_NIGHT,\"tariff_zone_id\":\"$TARIFF_NIGHT\"}]"################################################### --- 7. Отправка показаний с проверкой JSON ---##################################################SEND_RESPONSE=$(curl -s -k -X POST \ "https://lk.rks-energo.ru/api/personalAccount/$ACCOUNT_ID/devices/$DEVICE_ID/send" \ -H "Authorization: Bearer $TOKEN" \ -F "values=$VALUES")# Проверяем, что сервер вернул корректный JSONif ! echo "$SEND_RESPONSE" | jq . >/dev/null 2>&1; then fail "Сервер вернул не JSON:\n$SEND_RESPONSE"fiSTATUS=$(echo "$SEND_RESPONSE" | jq -r '.status // empty')################################################### --- 8. Telegram уведомления ---##################################################if [[ "$STATUS" == "1" ]]; then TEXT=$'✅ Показания электроэнергии успешно отправлены!\n\n☀️ День: '"$VALUE_DAY"$'\n🌙 Ночь: '"$VALUE_NIGHT" send_telegram "$TEXT"else ERROR_TEXT=$(echo "$SEND_RESPONSE" | jq -r '.data.message // "Неизвестная ошибка"') TEXT=$'❌ Ошибка отправки показаний РКС!\n\nОтвет сервера:\n'"$ERROR_TEXT"$'\n\n☀️ День: '"$VALUE_DAY"$'\n🌙 Ночь: '"$VALUE_NIGHT" send_telegram "$TEXT"fiexit 0
Фактически получился независимый контроль показаний электросчётчика.
Вода и компьютерное зрение
С водой ситуация оказалась проще. Мои счётчики не имеют импульсных выходов, поэтому классическое подключение было невозможно. Менять исправные приборы учёта ради автоматизации не хотелось. Поэтому я пошёл другим путём.
Для каждого водосчётчика установлена ESP32-CAM.
На ней работает система распознавания цифр по изображению, которая периодически фотографирует счётчик и определяет текущие показания.
Сразу отмечу, что саму систему компьютерного зрения я не разрабатывал с нуля.
За основу был взят проект AI-on-the-edge-device, который позволяет распознавать показания механических счётчиков прямо на ESP32-CAM. О самом проекте и его настройке подробно рассказывал Павел на своём сайте «У Павла!». Именно по этой статье я и запускал систему распознавания:
В моей системе этот проект был интегрирован в Home Assistant и стал частью общей автоматизации ЖКХ.
После распознавания данные отправляются в Home Assistant как обычные сенсоры.
По сути система делает то же самое, что раньше делал я сам: смотрит на цифры счётчика и записывает результат.
В HA выглядит вот так:

Разница только в том, что теперь это происходит автоматически.
BASH скрипт отправки показаний воды
#!/usr/bin/env bashset -euo pipefail############################### --- НАСТРОЙКИ ---##############################COOKIE_FILE="/config/scripts/water_cookies.cookie" # Netscape cookie fileTOKENS_FILE="/config/scripts/water_tokens.json" # валидный JSON с токенамиHOME_URL=""LOGIN_URL=""# Защита файловmkdir -p "$(dirname "$COOKIE_FILE")"touch "$COOKIE_FILE"chmod 600 "$COOKIE_FILE"touch "$TOKENS_FILE"chmod 600 "$TOKENS_FILE"#echo "==> 1) Получаем стартовые cookies (GET $HOME_URL)"curl -s -c "$COOKIE_FILE" -A "Mozilla/5.0" \ -H "Accept: */*" \ -H "Accept-Language: ru,en;q=0.9" \ "$HOME_URL" > /dev/null || true#echo "==> 2) Логинимся и сохраняем куки в $COOKIE_FILE"# Выполняем POST на логин; тело - JSON {username, password}''LOGIN_RESPONSE=$(curl -s -c "$COOKIE_FILE" -b "$COOKIE_FILE" -X POST "$LOGIN_URL" \ -H "Content-Type: application/json;charset=utf-8" \ -H "Accept: application/json, text/plain, */*" \ -H "Origin: https://xn----7sbdqbfldlsq5dd8p.xn--p1ai" \ -H "Referer: https://xn----7sbdqbfldlsq5dd8p.xn--p1ai/" \ -H "User-Agent: Mozilla/5.0" \ -d '{"username":"Логин","password":"Паоль"}')# Тело ответа (для отладки)#echo "Login response body:"#if echo "$LOGIN_RESPONSE" | jq . >/dev/null 2>&1; then# echo "$LOGIN_RESPONSE" | jq .#else# echo "$LOGIN_RESPONSE"#fi#echo####################################################### --- 3) Извлекаем токены из cookie-файла ---####################################################### Формат Netscape cookie: domain [tab] flag [tab] path [tab] secure [tab] expiry [tab] name [tab] value# Имя куки — в 6-м поле, значение — в 7-м поле (awk считает пробелы/табы)COOKIE_FILE="/config/scripts/water_cookies.cookie"TOKENS_FILE="/config/scripts/water_tokens.json"# гарантируем unix-формат (убираем возможные CR)# (необязательно — полезно, если куки приходят с CRLF)sed -i 's/\r$//' "$COOKIE_FILE" || trueget_cookie_value() { local name="$1" awk -v name="$name" -F $'\t' '$6==name{print $7}' "$COOKIE_FILE" 2>/dev/null | tail -n1 || true}CSRFTOKEN=$(get_cookie_value "csrftoken")ACCESS_TOKEN=$(get_cookie_value "access_token")REFRESH_TOKEN=$(get_cookie_value "refresh_token")#echo "Parsed cookie values:"#echo " CSRFTOKEN: ${CSRFTOKEN:-<none>}"#echo " ACCESS_TOKEN: ${ACCESS_TOKEN:-<none>}"#echo " REFRESH_TOKEN: ${REFRESH_TOKEN:-<none>}"jq -n \ --arg at "$ACCESS_TOKEN" \ --arg rt "$REFRESH_TOKEN" \ --arg ct "$CSRFTOKEN" \ '{ access_token: (if $at == "" then null else $at end), refresh_token: (if $rt == "" then null else $rt end), csrftoken: (if $ct == "" then null else $ct end) }' > "$TOKENS_FILE"chmod 600 "$TOKENS_FILE"#echo "Wrote tokens to $TOKENS_FILE"cat "$TOKENS_FILE"VALUE_COLD=$(curl -s -X GET "http://127.0.0.1:8123/api/states/sensor.khololdnaia_voda" \ -H "Authorization: Bearer токен HA" \ -H "Content-Type: application/json" | jq -r '.state'| awk '{printf("%d\n",$1)}')VALUE_HOT=$(curl -s -X GET "http://127.0.0.1:8123/api/states/sensor.goriachaia_voda" \ -H "Authorization: Bearer токен HA" \ -H "Content-Type: application/json" | jq -r '.state'| awk '{printf("%d\n",$1)}')####################################################### --- 4) Отправляем показания ---######################################################SEND_URL=""# формируем JSON телоPAYLOAD=$(jq -n \ --argjson cold "$VALUE_COLD" \ --argjson hot "$VALUE_HOT" \ '{ meters: [ {meter_id: "", values: [$cold]}, {meter_id: "", values: [$hot]} ] }')#echo "==> Отправляем показания: cold=$VALUE_COLD, hot=$VALUE_HOT"RESPONSE=$(curl -s -X POST "$SEND_URL" \ -H "Content-Type: application/json;charset=utf-8" \ -H "X-CSRFToken: $CSRFTOKEN" \ -b "$COOKIE_FILE" \ -d "$PAYLOAD")#echo "Ответ сервера:"#echo "$RESPONSE" | jq . || echo "$RESPONSE"####################################################### --- 6. Проверка и Telegram уведомление ---######################################################STATUS=$(echo "$RESPONSE" | jq -r '.status_code')if echo "$RESPONSE" | jq -e '.errors? | length > 0' >/dev/null; then PARSED_ERRORS=$(echo "$RESPONSE" | jq -r '.errors[]' \ | sed 's/Счётчик №01896 68/Холодная вода/g' \ | sed 's/Счётчик №01896 74/Горячая вода/g') TEXT=$'❌ Ошибка отправки показаний!\nОтвет сервера:\n'"$PARSED_ERRORS" curl -s -X POST "https://api.telegram.org/токен/sendMessage" \ -d chat_id= \ -d text="$TEXT" curl -s -X POST "https://api.telegram.org/токен/sendMessage" \ -d chat_id= \ -d text="$TEXT"else curl -s -X POST "https://api.telegram.org/токен/sendMessage" \ -d chat_id= \ -d text="✅ Показания водички успешно отправлены:❄️ Холодня - $VALUE_COLD m³🔥 Горячая - $VALUE_HOT m³" curl -s -X POST "https://api.telegram.org/токен/sendMessage" \ -d chat_id= \ -d text="✅ Показания водички успешно отправлены:❄️ Холодня - $VALUE_COLD m³🔥 Горячая - $VALUE_HOT m³"fi
Автоматическая передача показаний
Каждое 21 число Home Assistant запускает набор сценариев.
Для воды выполняется:
-
Авторизация на сайте.
-
Получение cookies и CSRF-токенов.
-
Чтение показаний из Home Assistant.
-
Формирование JSON-запроса.
-
Отправка показаний.
-
Анализ ответа сервера.
Если поставщик услуг отклонил показания, в Telegram приходит причина ошибки. Если всё прошло успешно — приходит подтверждение.

Аналогично работает отправка показаний электроэнергии.

Автоматическая проверка начислений
После передачи показаний работа системы не заканчивается.
Скрипты дополнительно заходят в личные кабинеты поставщиков услуг и собирают:
-
основной долг;
-
пени;
-
итоговую сумму;
-
ссылки на оплату.
Отдельно обрабатываются:
-
вода;
-
электроэнергия;
-
отопление;
-
капитальный ремонт.
Для отопления показания передавать не требуется, поэтому система просто получает начисления и формирует ссылку на оплату.
Telegram вместо четырёх личных кабинетов
В результате каждый месяц я получаю единый отчёт.
-
текущие показания;
-
задолженность;
-
ссылка на оплату.
Уведомление в телеграмм

Больше не нужно вспоминать адреса сайтов, логины и пароли или искать нужный раздел в личном кабинете. Всё уже собрано в одном сообщении. Ссылка на оплату открывает сразу форму оплаты где можно выбрать способ оплаты.
Итоги
Проект начинался как попытка не забывать передавать показания воды.
В итоге Home Assistant превратился в полноценную систему управления коммунальными услугами.
Сейчас она:
-
самостоятельно снимает показания;
-
самостоятельно передаёт показания;
-
контролирует начисления;
-
собирает ссылки на оплату;
-
сообщает об ошибках;
-
уведомляет о задолженностях.
А мне остаётся только открыть Telegram и нажать кнопку «Оплатить». Так же было реализовано получение квитанции сразу телеграмм бот, но практика показала что это лишняя информация именно в боте и IMAP интеграция HA оказалась высоконагруженной
PS: Такое можно реализовать и с ботами ВКонтакте.
ссылка на оригинал статьи https://habr.com/ru/articles/1041648/