Веб-мониторинг литиевых батарей по данным из Tuya Cloud API. Несколько устройств из одного аккаунта, наглядный дашборд, оценка времени до разряда. Python (FastAPI) + Docker. Всё настраивается через веб-панель администратора — реквизиты Tuya, поиск устройств, параметры — без правки файлов.
Заточено под DT20W 0–420V Tuya WiFi Smart Lithium Battery Capacity Tester (кулоновский счётчик), но работает с любым устройством Tuya: коды точек данных определяются автоматически из спецификации.
- Список устройств (главная): строка на батарею — имя, шкала и уровень заряда (SOC), режим, оценка времени до 30 %, напряжение и ток.
- Детальный дашборд по клику: SOC, напряжение, ток, мощность, температура, остаточная/полная ёмкость, живой график тока (заряд/разряд), таблица всех точек данных.
- Уведомления в Telegram: сообщение в чат, когда заряд упал ниже порога или когда до порога осталось меньше заданного времени (плюс «отбой» и потеря связи).
- Админ-панель
/admin: ввод реквизитов Tuya, проверка подключения, автопоиск устройств в аккаунте, управление списком, интервал опроса, демо-режим, оформление, настройка уведомлений, пароль доступа. - Пиксельный интерфейс, два набора тем (у каждого светлый и тёмный вариант): Dracula — по умолчанию (палитра Dracula и её официальный светлый вариант Alucard, гарнитура Meslo LG), «Пиксель» — приборный VFD-дисплей и монохромный LCD с построчной развёрткой. Тема по умолчанию задаётся в админ-панели, переключатель в шапке меняет оформление только в текущем браузере — см. Оформление.
- Крупные цифры заряда рисуются растровым шрифтом 5×7 прямо в SVG, поэтому для них внешние гарнитуры не нужны и всё работает офлайн.
- Автообновление, адаптив под телефон, запуск в Docker.
![]() |
![]() |
DEMO_MODE=true docker compose up --buildОткройте http://localhost:8000 — список из нескольких виртуальных батарей.
Без
.envи без реквизитов приложение само включает демо-режим.
- Запустите контейнер:
docker compose up --build -d
- Откройте http://localhost:8000/admin.
- Задайте пароль админ-панели (раздел «Безопасность»).
Либо заранее через переменную
ADMIN_PASSWORD(в.env). - В разделе «Подключение Tuya» введите Access ID, Access Secret и выберите регион, нажмите «Проверить подключение».
- Нажмите «Найти в аккаунте» — устройства подтянутся автоматически; выберите нужные и задайте им имена (или добавьте вручную по Device ID).
- Сохраните. Дашборд на http://localhost:8000 сразу покажет батареи.
Настройки сохраняются в файле ./data/settings.json в папке проекта
(/data/settings.json внутри контейнера) и переживают рестарт.
Контейнер работает под UID/GID 1000 — обычный UID первого пользователя Linux. Если
id -uна вашем хосте выдаёт другое значение, задайтеAPP_UIDиAPP_GIDв.env(см..env.example), иначе контейнер не сможет писать в./data.
Где взять Access ID / Secret и Device ID — в разделе «Настройка проекта в Tuya IoT Platform» ниже.
Чтобы приложение видело батареи, нужен Cloud-проект на Tuya с сервисом IoT Core и привязанным аккаунтом приложения Smart Life / Tuya Smart.
- Зарегистрируйтесь на https://iot.tuya.com.
- Cloud → Development → Create Cloud Project:
- Development Method — Smart Home (даёт нужный набор API);
- Data Center (регион) — тот же, что у аккаунта в приложении Smart Life /
Tuya Smart. Это же значение идёт в
TUYA_API_ENDPOINT(по умолчанию Европа).
- Вкладка Overview → скопируйте Access ID и Access Secret.
Приложению достаточно одного основного сервиса:
| API-сервис | Нужен | Зачем |
|---|---|---|
| IoT Core | ✅ обязательно | статус устройства, спецификации, инфо, список устройств |
| Authorization | ✅ (обычно по умолчанию) | получение access-токена |
| Device Status Notification | — | не нужен (приложение опрашивает само) |
Проверить: Cloud → Development → проект → Service API — в списке должен быть
IoT Core. Если его нет — Go to Authorize и добавьте.
Используемые эндпоинты (все относятся к IoT Core): /v1.0/token,
/v1.0/devices/{id}/status, /v1.0/devices/{id}/specifications,
/v1.0/devices/{id}, /v1.0/iot-01/associated-users/devices.
- Devices → Link App Account → отсканируйте QR-код приложением Smart Life (без этого шага устройств в проекте не будет).
- Devices → All Devices → скопируйте Device ID нужного устройства
(или используйте автопоиск в панели
/admin→ «Найти в аккаунте»).
Access ID / Secret / регион — в панели /admin → «Проверить подключение» →
«Найти в аккаунте». Либо через .env (TUYA_ACCESS_ID, TUYA_ACCESS_KEY,
TUYA_API_ENDPOINT).
- IoT Core — бесплатный триал (обычно ~1 месяц). Продление: Cloud → Cloud
Services → IoT Core → Extend Trial, заполнить короткую анкету → обычно +6 месяцев
(кнопка появляется ближе к концу срока). Когда и продлённый срок выйдет — платный
план или новый проект (Access ID/Secret сменятся — впишите новые в
/admin). - Ошибка Tuya
code 1106/permission deny/No permissions— почти всегда: не авторизован IoT Core, истёк триал, либо не привязан аккаунт (шаг 3). - Токен получаете, но устройств не видно — регион
TUYA_API_ENDPOINTне совпадает с регионом аккаунта.
Всё можно задать и без панели — на первом запуске это bootstrap-значения:
cp .env.example .env # TUYA_ACCESS_ID / SECRET / регион, ADMIN_PASSWORDСписок устройств — файлом config/devices.yml (см. config/devices.yml.example):
devices:
- id: "bf1234567890abcdef01"
name: "Гараж — LiFePO4 100Ah"
- id: "bf0987654321fedcba98"
name: "Дом — резервный АКБ 200Ah"или одним устройством: TUYA_DEVICE_ID + TUYA_DEVICE_NAME.
Приоритет значений: админ-панель (/data/settings.json) → переменные окружения / config/devices.yml.
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
DEMO_MODE=true uvicorn app.main:app --reload --port 8000Контейнер запускается с --proxy-headers --forwarded-allow-ips="*", поэтому
X-Forwarded-Proto учитывается и редиректы уходят на https://, а не на http://.
Порт публикуется только на loopback (127.0.0.1:8000:8000), поэтому напрямую из сети
приложение недоступно — весь трафик идёт через прокси.
Caddy запущен на хосте (обычный systemd-сервис) — этот вариант работает с настройками «из коробки»:
batt.example.com {
reverse_proxy 127.0.0.1:8000
}Caddy в контейнере — до 127.0.0.1 хоста он не достучится. Уберите публикацию порта
совсем (ports: → expose: - "8000"), поместите оба контейнера в одну docker-сеть и
проксируйте по имени сервиса:
batt.example.com {
reverse_proxy battery-monitor:8000
}Caddy сам проставляет X-Forwarded-Proto и сохраняет исходный Host, так что
дополнительных директив не нужно.
Важно:
--forwarded-allow-ips="*"означает «доверятьX-Forwarded-*от любого, кто может открыть соединение». Это безопасно ровно до тех пор, пока до приложения нельзя достучаться в обход прокси. Если вернёте публикацию порта на все интерфейсы ("8000:8000"), любой в сети сможет подделать эти заголовки.
Всё настраивается в админ-панели, раздел «Уведомления в Telegram» — ничего править в файлах не нужно.
- Создайте бота у @BotFather → команда
/newbot→ скопируйте токен вида123456789:AA.... - Напишите боту любое сообщение (для группы — добавьте бота в неё и напишите там).
- В админ-панели вставьте токен и нажмите «Определить» рядом с полем «ID чата» —
приложение подтянет chat_id из недавних сообщений. Можно ввести и вручную:
личный чат — положительное число, группа/канал — начинается с
-100. - Отметьте, какие уведомления слать, и нажмите «Отправить тестовое сообщение».
- Сохраните настройки.
Что можно включить:
| Уведомление | Когда приходит |
|---|---|
| Заряд ниже порога | SOC ≤ «порог заряда, %» |
| Мало времени до порога | до порога осталось меньше N минут (оценка по току разряда) |
| Возврат в норму | «отбой» после того, как показатель вернулся в норму |
| Устройство недоступно | ошибка опроса Tuya (и сообщение при восстановлении связи) |
Параметры: интервал проверки (как часто сверять пороги, по умолчанию 60 с) и повтор напоминания (0 — не повторять).
Антиспам: пока показатель не вернулся в норму, повторных сообщений нет (кроме явно заданного интервала повтора). У порогов есть гистерезис (2 % для заряда, +25 % для времени), поэтому у самой границы уведомления не «дребезжат». Последние отправленные сообщения и ошибки видны в журнале там же, в админ-панели.
Проверки идут в фоновом потоке приложения; после перезапуска контейнера состояние порогов начинается с чистого листа, поэтому активная тревога может прийти повторно.
Кулоновский метод: (остаточная_Ah − 0.30 × полная_Ah) / ток_разряда.
Ток берётся по знаку (отрицательный = разряд); если тока нет — оценивается как
мощность ÷ напряжение. Если устройство заряжается, простаивает или не отдаёт
ёмкость — вместо времени показывается соответствующий статус.
| Метод | Назначение |
|---|---|
GET / |
Список устройств |
GET /device/{id} |
Детальный дашборд устройства |
GET /admin |
Панель администратора |
GET /api/devices |
Сводка по всем устройствам (SOC, режим, ETA) |
GET /api/status?device_id= |
Полное состояние одного устройства |
GET /api/raw?device_id= |
Сырой статус + спецификация (отладка кодов DP) |
GET /api/config |
Настройки для фронтенда |
POST /api/admin/login · GET/POST /api/admin/settings · POST /api/admin/test-connection · GET /api/admin/discover · POST /api/admin/password |
Админ-API (защищены токеном при заданном пароле) |
POST /api/admin/telegram/test · POST /api/admin/telegram/chats · GET /api/admin/telegram/log |
Уведомления: тест, поиск chat_id, журнал |
GET /healthz |
Проверка работоспособности |
- Пароль админ-панели хранится хэшем (PBKDF2), доступ — по подписанному токену (HMAC). Дашборд и публичное API открыты (данные о батареях, не секреты).
- Access Secret, токен бота и настройки хранятся в
./data/settings.jsonв открытом виде — это нормально для self-hosted, но держите каталог./dataприватным и не коммитьте его содержимое (уже в.gitignore). Для доступа извне LAN используйте reverse-proxy с TLS.
Наборов тем два, у каждого светлый и тёмный вариант плюс «авто» — по настройке системы:
| Набор | Тёмная | Светлая | Особенности |
|---|---|---|---|
| Dracula (по умолчанию) | палитра Dracula | палитра Alucard | без построчной развёртки, гарнитура Meslo LG |
| Пиксель | приборный VFD-дисплей | монохромный LCD | построчная развёртка дисплея, системная моноширинная гарнитура |
![]() |
![]() |
![]() |
![]() |
Выбрать тему можно в двух местах, и они не мешают друг другу:
- Админ-панель → «Оформление» задаёт тему по умолчанию. Она хранится на
сервере (
themeвdata/settings.json, bootstrap — переменнаяTHEME) и действует на всех устройствах, где оформление не меняли вручную. - Выпадающий список в шапке любой страницы меняет оформление только в этом
браузере: выбор кладётся в
localStorage(ключbm-theme) и важнее темы по умолчанию. Вернуть общую тему — кнопкой «Вернуть тему по умолчанию» в той же карточке админ-панели.
Тема по умолчанию кэшируется в браузере (bm-theme-default) и применяется до
первой отрисовки, ещё до ответа /api/config, поэтому мигания при загрузке нет.
Вёрстка обеих тем работает на одном наборе CSS-переменных (app/static/pixel.css),
конкретный вариант ставит app/static/theme.js в атрибут data-theme на <html>:
dracula-dark · dracula-light · pixel-dark · pixel-light. Чтобы добавить свой
набор, достаточно описать ещё один блок переменных, пункт в списке CHOICES
(theme.js) и значение в THEMES (app/store.py).
Гарнитура Meslo LG M лежит в репозитории (app/static/fonts/, подмножество с
латиницей и кириллицей, ~22 КБ на начертание) и грузится только для тем Dracula;
если Meslo уже установлена в системе, используется она. Крупные цифры заряда
по-прежнему рисуются растровым шрифтом 5×7 в SVG и от гарнитуры не зависят.
battery-monitor/
├── app/
│ ├── main.py # FastAPI: страницы, публичное и админ API
│ ├── store.py # постоянные настройки + рантайм-состояние
│ ├── config.py # bootstrap из окружения
│ ├── auth.py # хэш пароля + токены
│ ├── tuya_client.py # клиент Tuya Cloud (подпись, токен, поиск устройств)
│ ├── devices.py # загрузка списка устройств
│ ├── metrics.py # нормализация DP + оценка времени до 30%
│ ├── notifier.py # Telegram: фоновая проверка порогов + отправка
│ ├── demo.py # демо-данные
│ └── static/ # index.html · device.html · admin.html
│ # pixel.css · pixel.js — общая пиксельная тема
│ # theme.js — выбор оформления (Dracula / Пиксель)
│ # fonts/ — Meslo LG M для тем Dracula
├── config/devices.yml.example
├── Dockerfile · docker-compose.yml · requirements.txt · .env.example
MIT © 2026 scatari69




