Как я научил Home Assistant передавать показания счётчиков и напоминать об оплате ЖКХ

от автора

Каждый месяц у меня была одна и та же задача:

  • снять и передать показания воды;

  • снять и передать показания электроэнергии;

  • проверить начисления;

  • найти ссылки на оплату;

  • оплатить счета.

Сам процесс занимал немного времени, но требовал внимания. Стоило забыть про очередное 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 запускает набор сценариев.

Для воды выполняется:

  1. Авторизация на сайте.

  2. Получение cookies и CSRF-токенов.

  3. Чтение показаний из Home Assistant.

  4. Формирование JSON-запроса.

  5. Отправка показаний.

  6. Анализ ответа сервера.

Если поставщик услуг отклонил показания, в Telegram приходит причина ошибки. Если всё прошло успешно — приходит подтверждение.

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

Автоматическая проверка начислений

После передачи показаний работа системы не заканчивается.

Скрипты дополнительно заходят в личные кабинеты поставщиков услуг и собирают:

  • основной долг;

  • пени;

  • итоговую сумму;

  • ссылки на оплату.

Отдельно обрабатываются:

  • вода;

  • электроэнергия;

  • отопление;

  • капитальный ремонт.

Для отопления показания передавать не требуется, поэтому система просто получает начисления и формирует ссылку на оплату.

Telegram вместо четырёх личных кабинетов

В результате каждый месяц я получаю единый отчёт.

  • текущие показания;

  • задолженность;

  • ссылка на оплату.

Уведомление в телеграмм

Больше не нужно вспоминать адреса сайтов, логины и пароли или искать нужный раздел в личном кабинете. Всё уже собрано в одном сообщении. Ссылка на оплату открывает сразу форму оплаты где можно выбрать способ оплаты.

Итоги

Проект начинался как попытка не забывать передавать показания воды.

В итоге Home Assistant превратился в полноценную систему управления коммунальными услугами.

Сейчас она:

  • самостоятельно снимает показания;

  • самостоятельно передаёт показания;

  • контролирует начисления;

  • собирает ссылки на оплату;

  • сообщает об ошибках;

  • уведомляет о задолженностях.

А мне остаётся только открыть Telegram и нажать кнопку «Оплатить». Так же было реализовано получение квитанции сразу телеграмм бот, но практика показала что это лишняя информация именно в боте и IMAP интеграция HA оказалась высоконагруженной

PS: Такое можно реализовать и с ботами ВКонтакте.

ссылка на оригинал статьи https://habr.com/ru/articles/1041648/