Skip to content

About

Автоматическая подача показаний в личный кабинет ЕИРЦ ЛО (`lk.epd47.ru`)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

SendEIRC

Автоматическая подача показаний в личный кабинет ЕИРЦ ЛО (lk.epd47.ru) и, параллельно, в кабинет Петроэлектросбыт (ikus.pesc.ru). Реальные показания берутся прямо со счётчика через API Waviot — вводить числа руками не нужно.

                                   ┌─►  ЕИРЦ (lk.epd47.ru)     python eirc.py
Waviot (счётчик)  ──►  округление ─┤    0000000000000 «день»
electro_..._t1                     │    0000000000001 «ночь»
electro_..._t2                     │
                                   └─►  ПЭС (ikus.pesc.ru)     python pesc.py
                                        scaleId 2 «День»
                                        scaleId 3 «Ночь»

Два кабинета — две независимые команды с одинаковым интерфейсом (--list, --dry-run, --days, --summary). Недоступность одного не отменяет подачу во второй.

Как это работает

Авторизация в ЕИРЦ

Публичного API у кабинета нет, поэтому скрипт воспроизводит веб-флоу. lk.epd47.ru — OIDC-клиент, id.epd47.ru — сервер авторизации. Кука сессии .AspNetCore.Cookies (чанкуется в C1/C2) выдаётся только ответом на POST /signin-oidc, и только если в сессии уже лежат .AspNetCore.Correlation.* и .AspNetCore.OpenIdConnect.Nonce.*.

Эти куки ставит сам lk в начале флоу, поэтому цепочка обязана начинаться с него, а state / nonce / code_challenge одноразовые — переиспользовать их из HAR нельзя.

1. GET  lk/ClientIdentity/Account/ExternalLogin?...&handler=Challenge
                                 -> 302 на authorize, ставит Correlation + Nonce
2. GET  id/connect/authorize     -> 302 на /Auth/Login
3. GET  id/Auth/Login            -> форма, __RequestVerificationToken
4. POST id/Auth/Login            -> 302 обратно на authorize
5. GET  id/connect/authorize     -> HTML-форма form_post (code, iss, state)
6. POST lk/signin-oidc           -> .AspNetCore.Cookies + C1 + C2
7. GET  lk/SN/YourIndications    -> счётчики
8. POST lk/SN/Result             -> подача

Счётчики не хардкодятся: они описаны data-атрибутами на странице показаний (data-id, data-service, data-number, data-name), которые один-в-один ложатся в payload.

Показания из Waviot

GET https://lk.waviot.ru/api.data/get_full_element_info/?id=<id>&key=<key>

Регистраторы: electro_ac_p_lsum_t1 — день, ..._t2 — ночь, ..._tsum — сумма. В конфиге можно писать псевдонимы день / ночь / сумма.

Авторизация в ПЭС и её двухфакторка

С ПЭС проще, чем с ЕИРЦ: под Angular-кабинетом живёт нормальный JSON API на /api, авторизация — Bearer-токеном. Никакого OIDC и парсинга HTML.

Мешает одно: вход всегда требует второй фактор — SMS, письмо, звонок или TOTP. Обходится он не взломом, а штатным механизмом самого кабинета: успешное подтверждение возвращает три токена.

Токен Что это
auth Bearer для всех запросов, живёт недолго
verified «это устройство уже подтверждено» — с ним можно логиниться заново без второго фактора
access назначение неясно, хранится как есть

Отсюда схема: код подтверждения вводится один раз, дальше скрипт молча перелогинивается по verified из pesc_tokens.json.

разовая настройка (python pesc.py --setup):
  1. POST api/v8/users/auth {login,password,type}          -> 424 + transactionId + types
  2. POST api/v7/users/{tx}/{type}/check/confirmation/send  -> придёт код
  3. POST api/v7/users/{tx}/{type}/check/verification {code} -> auth + verified + access

каждый запуск (python pesc.py):
     POST api/v8/users/auth + заголовок Auth-Verification: <verified>   -> 200 + auth

подача:
  GET  api/v6/accounts                                     лицевые счета
  GET  api/v6/accounts/{id}/meters/info                    счётчики, шкалы, прошлые
  POST api/v7/accounts/{id}/meters/{registration}/reading  [{"scaleId":N,"value":V}]

Тариф в ПЭС — это шкала (scale) внутри счётчика, а не отдельный счётчик, как в ЕИРЦ: день и ночь — две шкалы одного registration. Поэтому в конфиге адресация другая — scaleId, а не counterId.

Если verified протухнет или его отзовут, скрипт не станет молча пытаться дальше — напишет «выполните python pesc.py --setup» и вернёт код 1, то есть уйдёт обычным уведомлением об ошибке.

Установка

pip install -r requirements.txt
cp .env.example .env                  # логины + id/key Waviot
cp config.example.json config.json    # лицевые счета и счётчики

.env:

EIRC_LOGIN=ваш_логин
EIRC_PASSWORD=ваш_пароль
WAVIOT_ID=1234567
WAVIOT_KEY=...

# только для pesc.py, необязательно
PESC_LOGIN=+79990000000
PESC_PASSWORD=...

Узнать свои идентификаторы:

python eirc.py --list     # лицевые счета и counterId в ЕИРЦ
python eirc.py --meter    # регистраторы и текущие показания счётчика
python pesc.py --list     # лицевые счета, счётчики и scaleId в ПЭС

Для ПЭС перед --list нужен разовый вход (см. ниже) — иначе он честно скажет, что токенов нет.

Использование

python eirc.py --meter      # что показывает счётчик прямо сейчас
python eirc.py --list       # счета и последние поданные показания
python eirc.py --dry-run    # весь путь + payload, БЕЗ отправки
python eirc.py              # реальная подача
python eirc.py -v           # подробный лог редиректов

Источник значения

Для каждого счётчика в config.json задаётся ровно один из трёх:

Ключ Что делает
register берёт реальное показание со счётчика через Waviot
val подаёт указанное абсолютное число
increment прибавляет к последнему поданному в ЕИРЦ

Округление

Waviot отдаёт 668.9570, в ЕИРЦ подают целые кВт·ч. Режим задаётся полем round:

Режим 668.9570
floor 668
round 669
ceil (по умолчанию) 669
raw 668.957
2 668.96

ceil выбран по умолчанию: подаём с запасом, переплата возвращается перерасчётом на следующий месяц, а вот занижение копится долгом. Значение по умолчанию задано один раз — DEFAULT_ROUNDING в waviot.py.

Защита от отката

Счётчик не может крутиться назад. Если новое показание меньше уже поданного в ЕИРЦ, скрипт остановится:

Счётчик 00000000 день: новое показание 1230 МЕНЬШЕ уже поданного 1234.000

Это ловит и опечатки, и последствия ошибочных подач. Обойти — --allow-decrease.

Петроэлектросбыт (pesc.py)

Отдельная команда с тем же набором флагов. Waviot, округление и защита от отката — общие с eirc.py, отличается только «транспорт» подачи.

Разовая настройка

python pesc.py --setup
Способы подтверждения:
  PHONE      SMS
  EMAIL      письмо на e-mail
  FLASHCALL  звонок (код — последние 4 цифры номера)
Выберите [PHONE]:
Код отправлен: SMS
Введите код: 1234
Готово. Вошли как Иван Иванов. Токены сохранены в pesc_tokens.json
Дальше SMS не понадобится — запускайте python pesc.py

Способ можно задать сразу: --setup --confirm-type EMAIL. Сбросить токены и начать заново — python pesc.py --logout.

pesc_tokens.json — это ключ от кабинета. Он в .gitignore, на POSIX кладётся с правами 600. При переносе на другую машину проще выполнить --setup заново, чем копировать файл.

Дальше как обычно

python pesc.py --list       # лицевые счета, счётчики, scaleId и прошлые показания
python pesc.py --info       # услуги, окно приёма, счёт к оплате, баланс
python pesc.py --tariff     # ставки: сколько стоит кВт*ч днём и ночью
python pesc.py --period     # принимает ли кабинет показания прямо сейчас
python pesc.py --dry-run    # весь путь + payload, БЕЗ отправки
python pesc.py              # реальная подача
python pesc.py --summary    # короткий итог в stdout, лог в stderr

--list печатает ровно то, что нужно для конфига:

Лицевой счёт 000000000000 (id 1234567) — г. Санкт-Петербург, ...
  счётчик 00000000 (registration XXXXXXXXXX, ACTIVE)
    scaleId 2      День           последнее: 1234.0 кВт*ч  от 2026-08-20
    scaleId 3      Ночь           последнее: 567.0 кВт*ч   от 2026-08-20

Окно приёма показаний

--days — календарная эвристика: мы сами решаем, что подавать надо с 1 по 25. Но у ПЭС есть ручка, где кабинет отвечает на этот вопрос сам:

python pesc.py --period
Лицевой счёт 000000000000
  приём открыт, Сентябрь 2026 (01.09.2026 - 30.09.2026)
  поле deadLine кабинета: 10 (кабинет отдаёт его без пояснений)

Флаг forbidden из этого же ответа проверяется перед каждой подачей: если приём закрыт, скрипт не станет отправлять и вернёт ошибку с объяснением. Обойти — --ignore-period, выключить проверку совсем — "check_period": false. В --dry-run закрытый приём только предупреждает, чтобы пробный прогон работал в любой день.

Проверка мягкая по построению: ручка новая и на части счетов отвечает 404 — тогда она просто пропускается, и всё работает как раньше. Блокирует только явный forbidden.

Про deadLine честно: кабинет отдаёт это число (у ЛО — 10), но нигде его не поясняет, и во фронтенде ПЭС оно не отображается. Похоже на крайний день попадания в текущий расчёт, но это догадка, поэтому скрипт его показывает и никак не интерпретирует.

Сводка по счёту

python pesc.py --info
Лицевой счёт 000000000000 «квартира» (id 1234567)
  Услуги:        ELECTRICITY
  Показания:     приём открыт, Сентябрь 2026 (01.09.2026 - 30.09.2026)
  Счёт к оплате: 1234.56 руб. от 01.08.2026, доступен к скачиванию
  Расчёты:       Электроэнергия — баланс 0.00, начислено 0.00 (АО "ПСК")

Четыре независимых запроса, каждый в своём try: недоступность одного не скрывает остальные, вместо строки будет «не получено».

Стоимость кВт·ч

Отдельного эндпоинта про цену у кабинета нет — ставки лежат в карточке лицевого счёта, GET api/v7/accounts/{id}/details, блоком TABLE рядом с типом плиты и моделью счётчика. --tariff их оттуда достаёт:

Электроэнергия — Двухтарифный, руб. за единицу
                                  диапазон 1    диапазон 2    диапазон 3
  Круглосуточный                        6.70         12.84         17.26
  Ночь  23:00 — 07:00                   4.01          8.99         12.09
  День  07:00 — 23:00                   7.37         13.81         18.55

Цена не одна, их три: ставка растёт при превышении месячного объёма потребления. Сами пороги диапазонов кабинет в этом ответе не отдаёт — только ставки, поэтому какой диапазон применят, из API не узнать. rate_for(tariffs, "День") по умолчанию берёт первый, самый дешёвый.

Разбор устроен на два формата блоков: TABLE — многотарифный учёт (строки «День» / «Ночь» / «Круглосуточный», ставка на каждый диапазон), SOLID — одноставочные услуги вроде воды, где всё в строке «Тарифная ставка» через дробь. Незнакомые блоки просто пропускаются.

Ставка подмешивается и в итог подачи — в том числе в уведомление:

ПЭС: показания переданы
Лицевой счёт 000000000000
00000000 День: 1234.0 -> 1240 (+6 кВт*ч, 44.22 руб.)
00000000 Ночь: 567.0 -> 570 (+3 кВт*ч, 12.03 руб.)
Итого примерно 56.25 руб.

Это стоит одного дополнительного GET за прогон. Любая ошибка на нём гасится: тарифы не пришли — подача всё равно идёт, просто без сумм. Отключить — "cost": false в секции pesc, сменить диапазон — "band": "2".

Слово «примерно» тут не для скромности: берётся первый диапазон, а какой применят на самом деле, из API не видно.

Конфиг

Секция pesc в том же config.json:

"pesc": {
  "base_url": "https://ikus.pesc.ru",
  "customer": "IKUS-SPB",
  "login_type": "PHONE",
  "token_file": "pesc_tokens.json",
  "account": "000000000000",
  "readings": [
    { "scaleId": 2, "register": "день", "round": "ceil" },
    { "scaleId": 3, "register": "ночь", "round": "ceil" }
  ]
}

Вместо scaleId можно указать scaleName ("День"). Если счётчиков несколько — добавьте meter с серийным номером или registration. Источник значения (register / val / increment) и round — те же, что в разделах выше.

Кабинет юрлиц lk.pesc.ru — тот же API: поменяйте base_url, customer на spb и login_type на EMAIL или LOGIN.

Если понадобится TOTP

В кабинете есть и TOTP (api/v1/dfa/profile/totp/create включает, api/v1/dfa/{transactionId}/totp/verify подтверждает). Скрипт его не использует: verified решает ту же задачу и не требует ни секрета в .env, ни лишней зависимости. Если однажды verified начнёт протухать слишком часто, TOTP — готовый путь сделать вход полностью автоматическим даже после отзыва токена.

Расписание

Готовые конфигурации — в каталоге deploy/.

Обычный cron

ПЭС — каждый день в 23:45, ЕИРЦ — с 1 по 25 число в 23:50:

45 23 * * *    cd /config/sendeirc && /usr/bin/python3 pesc.py >> eirc.log 2>&1
50 23 1-25 * * cd /config/sendeirc && /usr/bin/python3 eirc.py >> eirc.log 2>&1

Поля: минута час день_месяца месяц день_недели. Диапазон 1-25 в третьем поле и даёт «с 1 по 25», * — каждый день.

Два кабинета — две строки, а не один скрипт с двумя адресатами: если ПЭС лежит, показания в ЕИРЦ всё равно уйдут, и в логе сразу видно, какой из двух не принял.

Почему у ПЭС расписание другое. Ежедневный запуск — это самовосстановление: упала сеть, лежал VPN, сервер был выключен — завтрашний запуск подхватит, и до конца месяца показания всё равно уйдут. Повторная подача ничего не ломает: счётчик растёт, защита от отката на месте. А календарь тут не нужен потому, что ПЭС сам отвечает, открыт ли приём (--period), и при закрытом окне скрипт не отправляет.

У ЕИРЦ такой ручки нет, а в кабинете написано, что в расчёт идут показания до 25-го, — поэтому там остаётся диапазон.

Два подводных камня:

  • cd обязателен. Скрипт читает .env, config.json и cookies.pkl из текущего каталога, а cron стартует из домашнего.
  • Путь к python — абсолютный. У cron урезанный PATH.

Таймзона берётся системная. Если сервер в UTC, а нужно 23:50 МСК — ставьте 50 20, либо добавьте в начало crontab CRON_TZ=Europe/Moscow.

Home Assistant

В HAOS обычного crontab нет. Три рабочих пути:

Способ Когда подходит
Автоматизация + shell_command скрипт лежит в /config, зависимости есть в контейнере HA
Add-on «Advanced SSH & Web Terminal» нужен привычный cron внутри HAOS
Отдельный Docker-контейнер самый изолированный, свои зависимости

Готовая автоматизация — deploy/homeassistant.yaml, контейнер — deploy/Dockerfile и deploy/docker-compose.yml.

Автоматизация HA коротко:

shell_command:
  send_eirc: "cd /config/sendeirc && python3 eirc.py --days 1-25 >> eirc.log 2>&1"
- alias: ЕИРЦ — подача показаний
  triggers:
    - trigger: time
      at: "23:50:00"
  conditions:
    - condition: template
      value_template: "{{ now().day <= 25 }}"
  actions:
    - action: shell_command.send_eirc

Для ПЭС в deploy/homeassistant.yaml лежит такая же пара (shell_command.send_pesc + автоматизация на 23:45, но ежедневная и без условия по числу месяца). Одно «но»: python pesc.py --setup из HA не запустить — код подтверждения нужно вводить руками, поэтому разовый вход делается через add-on «Advanced SSH & Web Terminal» или docker exec.

Уведомления в Telegram

Скрипт с --summary печатает в stdout только короткий итог, а весь лог отправляет в stderr — этот текст и уходит в сообщение:

$ python eirc.py --summary
ЕИРЦ: показания переданы
Лицевой счёт 000000000000
00000000 день: 1234.000 -> 1240
00000000 ночь: 567.000 -> 570

При ошибке — строка ОШИБКА ЕИРЦ: ... и код возврата 1, по которому автоматизация отличает провал от успеха. Если день вне диапазона --days, stdout начинается с SKIP — такие запуски можно не слать.

Готовая автоматизация — в deploy/homeassistant.yaml:

shell_command:
  send_eirc: >-
    /bin/sh -c 'cd /config/sendeirc &&
    python3 eirc.py --summary --days 1-25 2>> /config/sendeirc/eirc.log'

Обёртка /bin/sh -c обязательна. Если в команде нет шаблона {{ }}, HA не запускает шелл: он делает shlex.split и вызывает программу напрямую. Тогда cd ищется как несуществующий бинарник /bin/cd, и получается returncode 1 с пустыми stdout и stderr. По той же причине без обёртки не работают &&, | и 2>>.

actions:
  - action: shell_command.send_eirc
    response_variable: result
  - choose:
      - conditions: "{{ result.stdout is search('^SKIP') }}"
        sequence: []
      - conditions: "{{ result.returncode == 0 }}"
        sequence:
          - action: notify.send_message
            target:
              entity_id: notify.ВАШ_TELEGRAM
            data:
              message: "✅ {{ result.stdout }}"
    default:
      - action: notify.send_message
        target:
          entity_id: notify.ВАШ_TELEGRAM
        data:
          message: "⚠️ ЕИРЦ: подача не прошла
{{ result.stdout }}"
      - action: notify.mobile_app_ВАШ_ТЕЛЕФОН
        data:
          title: "⚠️ ЕИРЦ: подача не прошла"
          message: "{{ result.stdout }}"
          data:
            priority: high
            ttl: 0

Успех уходит только в Telegram, ошибка — ещё и пушем с priority: high, чтобы её нельзя было пропустить.

Если интеграция Telegram Bot уже настроена, токен брать негде и не нужно — он внутри интеграции. Повторно объявлять telegram_bot: в configuration.yaml нельзя, HA выдаст ошибку о дублирующей настройке. Нужно лишь узнать имя готового сервиса: Средства разработчика → Действия → набрать notify. У notify.send_message адресат задаётся через target.entity_id, и поля title там нет — заголовок пишется первой строкой message.

Если же Telegram ещё не настроен — токен у @BotFather, chat_id у @userinfobot, оба в secrets.yaml.

Подстраховка --days

Диапазон дней умеет проверять и сам скрипт:

python eirc.py --days 1-25

Вне диапазона он выходит с кодом 0, ничего не подавая. Полезно, если расписание сработает не в тот день — после перезагрузки, смены таймзоны или ручного запуска. Можно задать и через EIRC_DAYS в .env.

Важно: в кабинете написано, что при расчёте платы учитываются показания, переданные не позднее 25-го числа. Запуск в 23:50 25-го оставляет 10 минут запаса — если сайт будет недоступен, повтора уже не будет. Надёжнее 20-23 число.

У pesc.py флаг тоже есть (--days, PESC_DAYS), но по умолчанию он не используется: там подача ежедневная, а окно приёма спрашивается у кабинета. Это и снимает описанную выше проблему «одна попытка в последний день» — неудачный вечер просто повторится завтра.

Ошибка сертификата

Если подача падает так:

ОШИБКА ЕИРЦ: SSLError: HTTPSConnectionPool(host='id.epd47.ru', port=443): ...
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate

— дело не в скрипте и не в паролях. id.epd47.ru отдаёт только свой сертификат, без промежуточного GlobalSign, которым он подписан:

openssl s_client -connect id.epd47.ru:443 -servername id.epd47.ru </dev/null 2>/dev/null \
  | openssl x509 -noout -text | grep "CA Issuers"
#   CA Issuers - URI:http://secure.globalsign.com/cacert/gsgccr3dvtlsca2020.crt

Браузер такой сертификат догружает сам (AIA fetching), Python — нет. Пока промежуточный лежит в хранилище системы, всё работает; в контейнере Home Assistant его нет, и проверка цепочки обрывается.

Лечится своим набором корней — certifi плюс тот самый промежуточный. Собирается один раз, рядом со скриптом:

cd /config/custom_components/SendToEIRC

# 1. корни из certifi того интерпретатора, которым запускается скрипт
docker exec homeassistant python3 -c "import certifi;print(open(certifi.where()).read())" > /tmp/ca-bundle.pem

# 2. промежуточный GlobalSign, DER -> PEM
curl -fsSL http://secure.globalsign.com/cacert/gsgccr3dvtlsca2020.crt \
  | openssl x509 -inform DER >> /tmp/ca-bundle.pem

sudo cp /tmp/ca-bundle.pem .

# 3. проверка: должно быть ровно "Verify return code: 0 (ok)"
openssl s_client -connect id.epd47.ru:443 -servername id.epd47.ru -CAfile ca-bundle.pem </dev/null 2>/dev/null \
  | grep "Verify return code"

Файл с именем ca-bundle.pem рядом со скриптом (или в текущем каталоге) подхватывается сам — ни конфиг, ни команду запуска править не нужно. Он действует на все три клиента: ЕИРЦ, ПЭС и Waviot. Другое имя или другое место задаются полем ca_bundle в config.json (для ПЭС — либо то же поле верхнего уровня, либо своё внутри pesc) или переменной EIRC_CA_BUNDLE в .env. Если указанного файла нет, скрипт скажет об этом сразу, а не свалится посреди подачи.

Проверить, что стало лучше, можно без подачи:

python eirc.py --net-check

В .gitignore файл добавлен намеренно: набор корней свой у каждой машины, и certifi в нём со временем устаревает. Когда GlobalSign сменит промежуточный или обновится certifi — пересоберите файл теми же тремя командами.

Чего делать не стоит: отключать проверку (verify=False) или совать промежуточный сертификат в системное хранилище контейнера — при обновлении HA он оттуда пропадёт, и ошибка вернётся в самый неподходящий день.

Запасной путь, если логин сломается

Если вход по паролю перестанет работать (например, добавят капчу), куки можно взять из браузера — DevTools → Application → Cookies для lk.epd47.ru:

# cookies.txt: .AspNetCore.Cookies=chunks-2; .AspNetCore.CookiesC1=...; .AspNetCore.CookiesC2=...
python eirc.py --import-cookies cookies.txt

Файлы

eirc.py                     авторизация в ЕИРЦ, парсинг счётчиков, подача
pesc.py                     JSON API Петроэлектросбыт, обход 2FA через verified
waviot.py                   клиент Waviot, округление; работает и отдельно
pesc_tokens.json            токены ПЭС, создаётся при --setup (в .gitignore)
ca-bundle.pem               свой набор корней, если нужен (в .gitignore)
deploy/crontab              строки для cron с пояснениями
deploy/homeassistant.yaml   shell_command + автоматизация для HA
deploy/Dockerfile           самодостаточный контейнер со своим cron
deploy/docker-compose.yml   запуск контейнера

About

Автоматическая подача показаний в личный кабинет ЕИРЦ ЛО (`lk.epd47.ru`)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages