Skip to content

Repository files navigation

🔋 Battery Monitor — Tuya (DT20W)

License: MIT Python FastAPI Docker Tuya Cloud Last commit

Веб-мониторинг литиевых батарей по данным из 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.
Список Админ

Быстрый старт (демо, без настройки Tuya)

DEMO_MODE=true docker compose up --build

Откройте http://localhost:8000 — список из нескольких виртуальных батарей.

Без .env и без реквизитов приложение само включает демо-режим.


Настройка через админ-панель (рекомендуется)

  1. Запустите контейнер:
    docker compose up --build -d
  2. Откройте http://localhost:8000/admin.
  3. Задайте пароль админ-панели (раздел «Безопасность»). Либо заранее через переменную ADMIN_PASSWORD.env).
  4. В разделе «Подключение Tuya» введите Access ID, Access Secret и выберите регион, нажмите «Проверить подключение».
  5. Нажмите «Найти в аккаунте» — устройства подтянутся автоматически; выберите нужные и задайте им имена (или добавьте вручную по Device ID).
  6. Сохраните. Дашборд на 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» ниже.


Настройка проекта в Tuya IoT Platform

Чтобы приложение видело батареи, нужен Cloud-проект на Tuya с сервисом IoT Core и привязанным аккаунтом приложения Smart Life / Tuya Smart.

1. Создать проект

  1. Зарегистрируйтесь на https://iot.tuya.com.
  2. Cloud → Development → Create Cloud Project:
    • Development MethodSmart Home (даёт нужный набор API);
    • Data Center (регион) — тот же, что у аккаунта в приложении Smart Life / Tuya Smart. Это же значение идёт в TUYA_API_ENDPOINT (по умолчанию Европа).
  3. Вкладка Overview → скопируйте Access ID и Access Secret.

2. Подключить нужные API

Приложению достаточно одного основного сервиса:

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.

3. Привязать аккаунт и найти устройства

  1. Devices → Link App Account → отсканируйте QR-код приложением Smart Life (без этого шага устройств в проекте не будет).
  2. Devices → All Devices → скопируйте Device ID нужного устройства (или используйте автопоиск в панели /admin → «Найти в аккаунте»).

4. Ввести реквизиты в приложение

Access ID / Secret / регион — в панели /admin → «Проверить подключение» → «Найти в аккаунте». Либо через .env (TUYA_ACCESS_ID, TUYA_ACCESS_KEY, TUYA_API_ENDPOINT).

Триал IoT Core и частые ошибки

  • 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.


Запуск без Docker (разработка)

python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
DEMO_MODE=true uvicorn app.main:app --reload --port 8000

За реверс-прокси (Caddy, nginx)

Контейнер запускается с --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

Всё настраивается в админ-панели, раздел «Уведомления в Telegram» — ничего править в файлах не нужно.

  1. Создайте бота у @BotFather → команда /newbot → скопируйте токен вида 123456789:AA....
  2. Напишите боту любое сообщение (для группы — добавьте бота в неё и напишите там).
  3. В админ-панели вставьте токен и нажмите «Определить» рядом с полем «ID чата» — приложение подтянет chat_id из недавних сообщений. Можно ввести и вручную: личный чат — положительное число, группа/канал — начинается с -100.
  4. Отметьте, какие уведомления слать, и нажмите «Отправить тестовое сообщение».
  5. Сохраните настройки.

Что можно включить:

Уведомление Когда приходит
Заряд ниже порога SOC ≤ «порог заряда, %»
Мало времени до порога до порога осталось меньше N минут (оценка по току разряда)
Возврат в норму «отбой» после того, как показатель вернулся в норму
Устройство недоступно ошибка опроса Tuya (и сообщение при восстановлении связи)

Параметры: интервал проверки (как часто сверять пороги, по умолчанию 60 с) и повтор напоминания (0 — не повторять).

Антиспам: пока показатель не вернулся в норму, повторных сообщений нет (кроме явно заданного интервала повтора). У порогов есть гистерезис (2 % для заряда, +25 % для времени), поэтому у самой границы уведомления не «дребезжат». Последние отправленные сообщения и ошибки видны в журнале там же, в админ-панели.

Проверки идут в фоновом потоке приложения; после перезапуска контейнера состояние порогов начинается с чистого листа, поэтому активная тревога может прийти повторно.


Как считается «время до 30%»

Кулоновский метод: (остаточная_Ah − 0.30 × полная_Ah) / ток_разряда. Ток берётся по знаку (отрицательный = разряд); если тока нет — оценивается как мощность ÷ напряжение. Если устройство заряжается, простаивает или не отдаёт ёмкость — вместо времени показывается соответствующий статус.


API

Метод Назначение
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 построчная развёртка дисплея, системная моноширинная гарнитура
Dracula, тёмная Dracula, светлая
Пиксель, тёмная Пиксель, светлая

Выбрать тему можно в двух местах, и они не мешают друг другу:

  • Админ-панель → «Оформление» задаёт тему по умолчанию. Она хранится на сервере (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

About

Веб-мониторинг литиевых батарей Tuya (DT20W) с админ-панелью — несколько устройств, оценка времени до разряда. FastAPI + Docker.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages