mirror of
https://github.com/alexgetmancom/miband-bot.git
synced 2026-07-24 06:26:08 +03:00
feat: add one-click installers and Windows launcher
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
# Enforce CRLF for Windows batch files to prevent CMD parsing bugs on multi-byte UTF-8
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
|
# Enforce LF for shell scripts
|
||||||
|
*.sh text eol=lf
|
||||||
|
|
||||||
|
# Explicitly set default behavior for other files
|
||||||
|
* text=auto
|
||||||
@@ -20,3 +20,7 @@ status*.json
|
|||||||
*.zip
|
*.zip
|
||||||
*.tar
|
*.tar
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
|
||||||
|
# Generated launchers
|
||||||
|
run_local.sh
|
||||||
|
run_local.bat
|
||||||
|
|||||||
@@ -2,186 +2,195 @@
|
|||||||
|
|
||||||
Русский | [English](README_EN.md)
|
Русский | [English](README_EN.md)
|
||||||
|
|
||||||
Личный Telegram-бот для данных Xiaomi Fitness / Mi Band.
|
Личный self-hosted Telegram-бот для данных Xiaomi Fitness / Mi Band.
|
||||||
|
|
||||||
Он сам забирает шаги, сон, пульс и SpO2 из Xiaomi Fitness, складывает их в локальную SQLite-базу и показывает понятное меню в Telegram. Идея простая: данные с браслета остаются у вас на сервере, а Telegram становится удобной кнопкой «посмотреть здоровье за сегодня», «обновить вручную» или «выгрузить CSV».
|
Забирает шаги, сон, пульс и SpO2 из облака Xiaomi Fitness, хранит их
|
||||||
|
в локальной SQLite-базе и даёт доступ к ним прямо из Telegram —
|
||||||
|
без сторонних сервисов и без передачи данных третьим лицам.
|
||||||
|
|
||||||
Проект рассчитан на одного владельца. Это не публичный бот для многих пользователей и не медицинский сервис.
|
> **Проект рассчитан на одного владельца.**
|
||||||
|
> Это не публичный бот и не медицинский сервис.
|
||||||
|
|
||||||
## Что умеет
|
## Возможности
|
||||||
|
|
||||||
- Показывает последние шаги, сон, пульс и SpO2 в Telegram.
|
- Просмотр последних шагов, сна, пульса и SpO2 в Telegram.
|
||||||
- Запускает ручную синхронизацию кнопкой в меню.
|
- Ручная и автоматическая синхронизация по расписанию.
|
||||||
- Автоматически синхронизирует данные по расписанию.
|
- Хранение истории в SQLite (`data/`).
|
||||||
- Хранит историю в SQLite в папке `data/`.
|
- Экспорт всех таблиц в ZIP с CSV-файлами прямо в чат.
|
||||||
- Экспортирует накопленные таблицы в ZIP с CSV-файлами.
|
- Развёртывание через Docker Compose.
|
||||||
- Работает через Docker Compose.
|
- Атомарная запись Xiaomi-токена с правами `0600`.
|
||||||
- Пишет Xiaomi token атомарно с правами `0600`.
|
- Умная автопривязка к первому пользователю (whitelist).
|
||||||
|
|
||||||
## Кому подходит
|
|
||||||
|
|
||||||
Проект подойдет, если вы:
|
|
||||||
|
|
||||||
- пользуетесь Xiaomi Fitness / Mi Band;
|
|
||||||
- хотите видеть свои данные в Telegram;
|
|
||||||
- готовы запустить маленький self-hosted сервис;
|
|
||||||
- понимаете, что неофициальные API могут сломаться после изменений Xiaomi.
|
|
||||||
|
|
||||||
Проект не подойдет, если нужен многопользовательский SaaS, медицинская точность, гарантия совместимости со всеми браслетами или официальный Xiaomi API.
|
|
||||||
|
|
||||||
## Важно про reverse engineering
|
|
||||||
|
|
||||||
`miband-bot` - неофициальный проект. Он не связан с Xiaomi, Zepp, Huami, Telegram или их партнерами.
|
|
||||||
|
|
||||||
Доступ к данным Xiaomi Fitness сделан через reverse engineering неофициальных API. Это значит:
|
|
||||||
|
|
||||||
- Xiaomi может изменить API без предупреждения;
|
|
||||||
- вход или синхронизация могут временно перестать работать;
|
|
||||||
- используйте проект только для своих аккаунтов и своих данных;
|
|
||||||
- соблюдайте применимые законы и условия сервисов в вашей стране;
|
|
||||||
- данные браслета не являются медицинским заключением.
|
|
||||||
|
|
||||||
## Как это работает
|
## Как это работает
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Mi Band -> Xiaomi Fitness cloud -> miband-bot -> SQLite -> Telegram menu / CSV export
|
Mi Band → Xiaomi Fitness cloud → miband-bot → SQLite → Telegram / CSV
|
||||||
```
|
```
|
||||||
|
|
||||||
В Docker Compose запускаются два процесса:
|
Docker Compose запускает два процесса:
|
||||||
|
|
||||||
- `tracker` - периодически синхронизирует данные из Xiaomi Fitness;
|
- `tracker` — периодически синхронизирует данные из Xiaomi Fitness;
|
||||||
- `fitness-bot` - отвечает в Telegram, показывает меню, запускает ручной sync и экспорт.
|
- `fitness-bot` — обслуживает Telegram-меню, ручной sync и экспорт.
|
||||||
|
|
||||||
Оба процесса используют одну папку `./data`. Запись защищена файловым lock, поэтому фоновая и ручная синхронизация не пишут в SQLite/token одновременно.
|
Оба процесса работают с одной папкой `./data`. Конкурентная запись
|
||||||
|
исключена файловым lock-ом.
|
||||||
|
|
||||||
## Что понадобится
|
## Требования
|
||||||
|
|
||||||
- Сервер или домашняя машина с Docker и Docker Compose.
|
- Docker и Docker Compose (или установленный Python 3.10+).
|
||||||
- Telegram bot token от [@BotFather](https://t.me/BotFather).
|
- Telegram bot token от [@BotFather](https://t.me/BotFather).
|
||||||
- Ваш Telegram user id.
|
- Аккаунт Xiaomi с данными Xiaomi Fitness.
|
||||||
- Xiaomi аккаунт, в котором видны данные Xiaomi Fitness.
|
|
||||||
|
|
||||||
## Быстрый запуск
|
## Быстрый запуск
|
||||||
|
|
||||||
1. Скопируйте пример секретов:
|
### Способ 1: Бесшовная установка в один клик (Рекомендуется)
|
||||||
|
|
||||||
```sh
|
Если у вас еще нет проекта на компьютере, вы можете автоматически скачать и настроить его одной командой в терминале:
|
||||||
cp secrets.env.example secrets.env
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Заполните минимум эти переменные:
|
- **macOS / Linux:**
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/iAlexeyRu/miband-bot/main/install.sh | bash
|
||||||
|
```
|
||||||
|
- **Windows (PowerShell):**
|
||||||
|
```powershell
|
||||||
|
powershell -c "irm https://raw.githubusercontent.com/iAlexeyRu/miband-bot/main/install.ps1 | iex"
|
||||||
|
```
|
||||||
|
|
||||||
```env
|
Установщик сам создаст папку `miband-bot`, загрузит и распакует файлы проекта, проверит окружение и запустит интерактивную настройку!
|
||||||
TELEGRAM_BOT_TOKEN=123456:replace-me
|
Повторный запуск этой же PowerShell-команды в уже настроенной установке обновит файлы и сразу запустит бота без повторного ввода токена.
|
||||||
TELEGRAM_ALLOWED_USER_ID=123456789
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Запустите сервис:
|
---
|
||||||
|
|
||||||
```sh
|
### Способ 2: Запуск из скачанной папки
|
||||||
docker compose up -d --build
|
|
||||||
docker compose logs -f fitness-bot
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Откройте своего Telegram-бота и отправьте `/start`.
|
Если вы уже склонировали репозиторий через `git clone` или скачали архив вручную:
|
||||||
|
|
||||||
5. Бот покажет кнопку входа в Xiaomi. Подтвердите вход по ссылке/QR. После этого бот сохранит `data/token_<telegram_user_id>.json`, запустит первую синхронизацию и откроет главное меню.
|
- **macOS / Linux:**
|
||||||
|
```sh
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
- **Windows:**
|
||||||
|
Запустите двойным кликом файл `setup.bat` или выполните в консоли:
|
||||||
|
```cmd
|
||||||
|
setup.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт сам проверит окружение, пошагово поможет получить токен, создаст конфигурацию `secrets.env`, развернет окружение Python (если выбран запуск без Docker) и предложит запустить бота одной кнопкой.
|
||||||
|
После настройки бот можно запускать повторно через `run_local.bat` из папки `miband-bot`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Способ 3: Полностью ручная настройка (manual setup):
|
||||||
|
|
||||||
|
1. Скопируйте шаблон конфигурации:
|
||||||
|
```sh
|
||||||
|
cp secrets.env.example secrets.env
|
||||||
|
```
|
||||||
|
2. Укажите ваш `TELEGRAM_BOT_TOKEN` в файле `secrets.env`. Переменную `TELEGRAM_ALLOWED_USER_ID` **оставьте пустой** — бот автоматически привяжется к вам при первом старте.
|
||||||
|
3. Запустите Docker контейнеры:
|
||||||
|
```sh
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
4. Откройте вашего созданного бота в Telegram и отправьте ему команду `/start` — бот распознает ваш аккаунт, привяжет его как единственного владельца и начнет синхронизацию!
|
||||||
|
|
||||||
## Настройки
|
## Настройки
|
||||||
|
|
||||||
Основные переменные лежат в `secrets.env`:
|
Все переменные — в `secrets.env`:
|
||||||
|
|
||||||
```env
|
| Переменная | По умолчанию | Описание |
|
||||||
TELEGRAM_BOT_TOKEN=123456:replace-me
|
| -------------------------- | ------------ | --------------------------------------- |
|
||||||
TELEGRAM_ALLOWED_USER_ID=123456789
|
| `TELEGRAM_BOT_TOKEN` | — | Token Telegram-бота |
|
||||||
SYNC_INTERVAL=900
|
| `TELEGRAM_ALLOWED_USER_ID` | — | Разрешённый user id (оставьте пустым для автопривязки) |
|
||||||
QUERY_DURATION=2
|
| `SYNC_INTERVAL` | `900` | Интервал фоновой синхронизации, секунды |
|
||||||
ENABLE_FDS_SLEEP_DETAILS=true
|
| `QUERY_DURATION` | `2` | Глубина запроса при sync, дней |
|
||||||
```
|
| `ENABLE_FDS_SLEEP_DETAILS` | `true` | Загружать детальные ночные данные FDS |
|
||||||
|
|
||||||
- `TELEGRAM_BOT_TOKEN` - token вашего Telegram-бота.
|
Пути к базе и статусу заданы в `compose.yaml`. При запуске без Docker
|
||||||
- `TELEGRAM_ALLOWED_USER_ID` - единственный Telegram user id, которому разрешен доступ.
|
смотрите `secrets.env.example`.
|
||||||
- `SYNC_INTERVAL` - интервал фоновой синхронизации в секундах. `900` = 15 минут.
|
|
||||||
- `QUERY_DURATION` - сколько последних дней запрашивать при sync.
|
|
||||||
- `ENABLE_FDS_SLEEP_DETAILS` - пробовать ли загружать детальные ночные данные FDS.
|
|
||||||
|
|
||||||
Пути к базе и статусу уже заданы в `compose.yaml`. Если запускаете без Docker, смотрите `secrets.env.example`.
|
## Файлы данных
|
||||||
|
|
||||||
## Где лежат данные
|
|
||||||
|
|
||||||
Runtime-файлы создаются в `./data`:
|
Runtime-файлы создаются в `./data`:
|
||||||
|
|
||||||
- `token_<telegram_user_id>.json` - Xiaomi auth token, секретный файл;
|
| Файл | Содержимое |
|
||||||
- `miband_<telegram_user_id>.db` - SQLite-база с health-данными;
|
| ---------------------- | --------------------------------- |
|
||||||
- `status_<telegram_user_id>.json` - последний статус синхронизации;
|
| `token_<id>.json` | Xiaomi auth token (**секретный**) |
|
||||||
- `fitness_bot_state.db` - служебное состояние Telegram-меню;
|
| `miband_<id>.db` | SQLite-база с health-данными |
|
||||||
- `sync_<telegram_user_id>.lock` - lock-файл синхронизации.
|
| `status_<id>.json` | Последний статус синхронизации |
|
||||||
|
| `allowed_user.id` | ID привязанного владельца |
|
||||||
|
| `fitness_bot_state.db` | Служебное состояние Telegram-меню |
|
||||||
|
| `sync_<id>.lock` | Lock-файл синхронизации |
|
||||||
|
|
||||||
Не коммитьте `secrets.env`, `data/`, `*.db`, `token*.json` и `status*.json`. Эти файлы уже добавлены в `.gitignore`.
|
`secrets.env`, `data/`, `*.db`, `token*.json` и `status*.json`
|
||||||
|
добавлены в `.gitignore` — не коммитьте их.
|
||||||
|
|
||||||
## Команды бота
|
## Команды
|
||||||
|
|
||||||
- `/start` - открыть меню или начать вход в Xiaomi.
|
| Команда | Действие |
|
||||||
- `/sync` - запустить ручную синхронизацию.
|
| --------- | ------------------------------------- |
|
||||||
- `/status` - показать состояние локальной базы.
|
| `/start` | Открыть меню или начать вход в Xiaomi |
|
||||||
|
| `/sync` | Запустить ручную синхронизацию |
|
||||||
Основное управление происходит кнопками в Telegram-меню.
|
| `/status` | Показать состояние локальной базы |
|
||||||
|
|
||||||
## Экспорт CSV
|
|
||||||
|
|
||||||
В меню есть экспорт данных. Бот собирает ZIP с CSV-таблицами и отправляет его в Telegram.
|
|
||||||
|
|
||||||
Помните: ZIP с health-данными уходит через инфраструктуру Telegram. Не отправляйте экспорт в чужие чаты и не храните его там, где доступ есть у других людей.
|
|
||||||
|
|
||||||
## Локальная разработка
|
## Локальная разработка
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 -m venv .venv
|
python3 -m venv .venv
|
||||||
.venv/bin/pip install -r requirements-dev.txt -e mi-fitness-python
|
.venv/bin/pip install -r requirements-dev.txt -e mi-fitness-python
|
||||||
.venv/bin/python -m py_compile fitness_bot.py miband_sync.py $(find miband_tracker -name '*.py' | sort)
|
.venv/bin/python -m py_compile fitness_bot.py miband_sync.py \
|
||||||
|
$(find miband_tracker -name '*.py' | sort)
|
||||||
.venv/bin/python -m pytest
|
.venv/bin/python -m pytest
|
||||||
.venv/bin/python -m pytest mi-fitness-python/tests/unit
|
.venv/bin/python -m pytest mi-fitness-python/tests/unit
|
||||||
.venv/bin/ruff check .
|
.venv/bin/ruff check .
|
||||||
.venv/bin/python -m pip check
|
.venv/bin/python -m pip check
|
||||||
```
|
```
|
||||||
|
|
||||||
Entrypoints сохранены для Docker и локального запуска:
|
Точки входа:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python -u miband_sync.py
|
python -u miband_sync.py # или: miband-sync
|
||||||
python -u fitness_bot.py
|
python -u fitness_bot.py # или: miband-fitness-bot
|
||||||
```
|
|
||||||
|
|
||||||
Если проект установлен как Python package, доступны console scripts:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
miband-sync
|
|
||||||
miband-fitness-bot
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**Бот не отвечает.**
|
**Бот не отвечает** — проверьте `TELEGRAM_BOT_TOKEN`, логи, а также убедитесь, что вы первыми отправили `/start` боту для привязки. При необходимости сбросить привязанного владельца просто удалите файл `data/allowed_user.id` и отправьте `/start` снова.
|
||||||
Проверьте `TELEGRAM_BOT_TOKEN`, `TELEGRAM_ALLOWED_USER_ID` и логи:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker compose logs -f fitness-bot
|
docker compose logs -f fitness-bot
|
||||||
```
|
```
|
||||||
|
|
||||||
**Синхронизация пишет, что token не найден.**
|
**Token не найден** — отправьте `/start` и пройдите Xiaomi login flow.
|
||||||
Откройте бота в Telegram, отправьте `/start` и пройдите Xiaomi login flow.
|
|
||||||
|
|
||||||
**Token истек.**
|
**Token истёк** — запустите повторный вход из меню; старый файл
|
||||||
В меню запустите повторный вход в Xiaomi. Старый token можно удалить из `data/`.
|
можно удалить из `data/`.
|
||||||
|
|
||||||
**Данных мало или нет SpO2/деталей сна.**
|
**Нет SpO2 или деталей сна** — убедитесь, что эти данные отображаются
|
||||||
Проверьте, что Xiaomi Fitness реально показывает эти данные. Часть данных зависит от модели браслета, настроек шаринга и доступности неофициального API.
|
в самом приложении Xiaomi Fitness. Доступность зависит от модели
|
||||||
|
браслета и настроек шаринга.
|
||||||
|
|
||||||
**После обновления Xiaomi все сломалось.**
|
**После обновления Xiaomi всё сломалось** — это ожидаемый риск
|
||||||
Это ожидаемый риск reverse-engineering проекта. Проверьте issues/README и логи, затем обновите код или временно отключите проблемную часть.
|
при работе с неофициальным API. Проверьте issues и логи, затем
|
||||||
|
обновите код или временно отключите проблемный модуль.
|
||||||
|
|
||||||
## Лицензия и vendored SDK
|
## Важно: reverse engineering и ограничения
|
||||||
|
|
||||||
Проект распространяется под GNU GPL v3.0 or later. Полный текст лицензии лежит в [LICENSE](LICENSE).
|
`miband-bot` — неофициальный проект, не связанный с Xiaomi, Zepp,
|
||||||
|
Huami или Telegram.
|
||||||
|
|
||||||
SDK `mi-fitness-python` хранится в репозитории как vendored source copy и остается под своей GNU GPL v3.0 лицензией: [mi-fitness-python/LICENSE](mi-fitness-python/LICENSE). Подробности о происхождении и политике обновления описаны в [VENDORED.md](VENDORED.md).
|
Доступ к данным реализован через reverse engineering закрытых API,
|
||||||
|
поэтому:
|
||||||
|
|
||||||
|
- Xiaomi может изменить API без предупреждения;
|
||||||
|
- авторизация или синхронизация могут временно не работать;
|
||||||
|
- используйте проект только со своими аккаунтами и данными;
|
||||||
|
- соблюдайте законодательство и условия использования сервисов;
|
||||||
|
- данные браслета не являются медицинским заключением.
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Проект распространяется под [GNU GPL v3.0 or later](LICENSE).
|
||||||
|
|
||||||
|
SDK `mi-fitness-python` включён как vendored source copy под
|
||||||
|
[GNU GPL v3.0](mi-fitness-python/LICENSE). Подробности — в
|
||||||
|
[VENDORED.md](VENDORED.md).
|
||||||
|
|||||||
+122
-115
@@ -2,186 +2,193 @@
|
|||||||
|
|
||||||
[Русский](README.md) | English
|
[Русский](README.md) | English
|
||||||
|
|
||||||
A personal Telegram bot for Xiaomi Fitness / Mi Band data.
|
A personal self-hosted Telegram bot for your Xiaomi Fitness / Mi Band data.
|
||||||
|
|
||||||
It fetches steps, sleep, heart rate and SpO2 from Xiaomi Fitness, stores the history in a local SQLite database, and shows a practical Telegram menu. The goal is simple: your band data stays on your own server, while Telegram becomes a convenient place to check today's health snapshot, run a manual sync or export CSV files.
|
Fetches steps, sleep, heart rate, and SpO2 from the Xiaomi Fitness cloud,
|
||||||
|
stores them in a local SQLite database, and provides access to them directly from Telegram —
|
||||||
|
without third-party services and without sharing your data with anyone.
|
||||||
|
|
||||||
The project is designed for one owner. It is not a public multi-user bot and not a medical service.
|
> **This project is designed for a single owner.**
|
||||||
|
> This is not a public bot or a medical service.
|
||||||
|
|
||||||
## What It Does
|
## Features
|
||||||
|
|
||||||
- Shows recent steps, sleep, heart rate and SpO2 in Telegram.
|
- View recent steps, sleep, heart rate, and SpO2 in Telegram.
|
||||||
- Runs manual sync from a Telegram button.
|
- Manual and scheduled automatic synchronization.
|
||||||
- Syncs data automatically on a schedule.
|
- History storage in SQLite (`data/`).
|
||||||
- Stores history in SQLite under `data/`.
|
- Export of all tables to a ZIP archive with CSV files directly into the chat.
|
||||||
- Exports accumulated tables as a ZIP with CSV files.
|
- Deployment via Docker Compose.
|
||||||
- Runs with Docker Compose.
|
- Atomic writing of the Xiaomi token with `0600` permissions.
|
||||||
- Writes the Xiaomi token atomically with mode `0600`.
|
- Smart auto-binding to the first user (whitelist).
|
||||||
|
|
||||||
## Who It Is For
|
## How it works
|
||||||
|
|
||||||
This project is useful if you:
|
|
||||||
|
|
||||||
- use Xiaomi Fitness / Mi Band;
|
|
||||||
- want to see your own data in Telegram;
|
|
||||||
- are comfortable running a small self-hosted service;
|
|
||||||
- understand that unofficial APIs can break when Xiaomi changes something.
|
|
||||||
|
|
||||||
It is not a good fit if you need a multi-user SaaS, medical-grade accuracy, guaranteed compatibility with every band, or an official Xiaomi API.
|
|
||||||
|
|
||||||
## Reverse Engineering Notice
|
|
||||||
|
|
||||||
`miband-bot` is an unofficial project. It is not affiliated with Xiaomi, Zepp, Huami, Telegram or their partners.
|
|
||||||
|
|
||||||
Xiaomi Fitness access is based on reverse engineering of unofficial APIs. This means:
|
|
||||||
|
|
||||||
- Xiaomi can change the API without notice;
|
|
||||||
- login or sync can temporarily stop working;
|
|
||||||
- use the project only with your own accounts and your own data;
|
|
||||||
- follow applicable laws and service terms in your jurisdiction;
|
|
||||||
- wearable data is not a medical diagnosis.
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Mi Band -> Xiaomi Fitness cloud -> miband-bot -> SQLite -> Telegram menu / CSV export
|
Mi Band → Xiaomi Fitness cloud → miband-bot → SQLite → Telegram / CSV
|
||||||
```
|
```
|
||||||
|
|
||||||
Docker Compose starts two processes:
|
Docker Compose runs two processes:
|
||||||
|
|
||||||
- `tracker` - periodically syncs data from Xiaomi Fitness;
|
- `tracker` — periodically synchronizes data from Xiaomi Fitness;
|
||||||
- `fitness-bot` - responds in Telegram, shows the menu, starts manual sync and exports data.
|
- `fitness-bot` — serves the Telegram menu, handles manual sync, and performs exports.
|
||||||
|
|
||||||
Both processes share `./data`. Writes are protected by a file lock, so background sync and manual sync do not write SQLite/token files at the same time.
|
Both processes work with the same `./data` folder. Concurrent write access
|
||||||
|
is prevented by a file-based lock.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- A server or home machine with Docker and Docker Compose.
|
- Docker and Docker Compose (or installed Python 3.10+).
|
||||||
- A Telegram bot token from [@BotFather](https://t.me/BotFather).
|
- Telegram bot token from [@BotFather](https://t.me/BotFather).
|
||||||
- Your Telegram user id.
|
|
||||||
- A Xiaomi account with Xiaomi Fitness data.
|
- A Xiaomi account with Xiaomi Fitness data.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
1. Copy the secrets template:
|
### Method 1: Seamless One-Click Installation (Recommended)
|
||||||
|
|
||||||
```sh
|
If you don't have the project files on your machine yet, you can automatically download and set everything up using a single command in your terminal:
|
||||||
cp secrets.env.example secrets.env
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Fill at least these variables:
|
- **macOS / Linux:**
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/iAlexeyRu/miband-bot/main/install.sh | bash
|
||||||
|
```
|
||||||
|
- **Windows (PowerShell):**
|
||||||
|
```powershell
|
||||||
|
powershell -c "irm https://raw.githubusercontent.com/iAlexeyRu/miband-bot/main/install.ps1 | iex"
|
||||||
|
```
|
||||||
|
|
||||||
```env
|
The installer will automatically create a `miband-bot` directory, download and extract the project files, verify dependencies, and launch the interactive setup!
|
||||||
TELEGRAM_BOT_TOKEN=123456:replace-me
|
Running the same PowerShell command again on an already configured install will update the files and start the bot without asking for the Telegram token again.
|
||||||
TELEGRAM_ALLOWED_USER_ID=123456789
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Start the service:
|
---
|
||||||
|
|
||||||
```sh
|
### Method 2: Launch from Downloaded Directory
|
||||||
docker compose up -d --build
|
|
||||||
docker compose logs -f fitness-bot
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Open your Telegram bot and send `/start`.
|
If you have already cloned the repository via `git clone` or downloaded the ZIP archive manually:
|
||||||
|
|
||||||
5. The bot will show a Xiaomi login button. Confirm login through the link/QR flow. After that the bot saves `data/token_<telegram_user_id>.json`, runs the first sync and opens the main menu.
|
- **macOS / Linux:**
|
||||||
|
```sh
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
- **Windows:**
|
||||||
|
Double-click the `setup.bat` file or run it in the console:
|
||||||
|
```cmd
|
||||||
|
setup.bat
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration
|
The script will automatically check your environment, guide you step-by-step to get your Telegram bot token, create the `secrets.env` configuration, set up the Python virtual environment (if you choose to run without Docker), and let you launch the bot with a single key press!
|
||||||
|
After setup, you can start the bot again with `run_local.bat` from the `miband-bot` folder.
|
||||||
|
|
||||||
Main variables live in `secrets.env`:
|
---
|
||||||
|
|
||||||
```env
|
### Method 3: Fully Manual Setup:
|
||||||
TELEGRAM_BOT_TOKEN=123456:replace-me
|
|
||||||
TELEGRAM_ALLOWED_USER_ID=123456789
|
|
||||||
SYNC_INTERVAL=900
|
|
||||||
QUERY_DURATION=2
|
|
||||||
ENABLE_FDS_SLEEP_DETAILS=true
|
|
||||||
```
|
|
||||||
|
|
||||||
- `TELEGRAM_BOT_TOKEN` - your Telegram bot token.
|
1. Copy the configuration template:
|
||||||
- `TELEGRAM_ALLOWED_USER_ID` - the single Telegram user id allowed to access the bot.
|
```sh
|
||||||
- `SYNC_INTERVAL` - background sync interval in seconds. `900` = 15 minutes.
|
cp secrets.env.example secrets.env
|
||||||
- `QUERY_DURATION` - how many recent days to query on each sync.
|
```
|
||||||
- `ENABLE_FDS_SLEEP_DETAILS` - whether to try fetching detailed FDS sleep data.
|
2. Specify your `TELEGRAM_BOT_TOKEN` in the `secrets.env` file. **Leave the `TELEGRAM_ALLOWED_USER_ID` variable blank** — the bot will automatically bind to you upon the first start.
|
||||||
|
3. Start the Docker containers:
|
||||||
|
```sh
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
4. Open your created bot in Telegram and send the `/start` command — the bot will recognize your account, bind it as the sole owner, and begin synchronization!
|
||||||
|
|
||||||
Database and status paths are already set in `compose.yaml`. If you run without Docker, see `secrets.env.example`.
|
## Settings
|
||||||
|
|
||||||
## Where Data Lives
|
All variables are in `secrets.env`:
|
||||||
|
|
||||||
Runtime files are created under `./data`:
|
| Variable | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `TELEGRAM_BOT_TOKEN` | — | Telegram bot token |
|
||||||
|
| `TELEGRAM_ALLOWED_USER_ID` | — | Allowed user ID (leave empty for auto-binding) |
|
||||||
|
| `SYNC_INTERVAL` | `900` | Background sync interval, in seconds |
|
||||||
|
| `QUERY_DURATION` | `2` | Fetch depth during sync, in days |
|
||||||
|
| `ENABLE_FDS_SLEEP_DETAILS` | `true` | Download detailed FDS night sleep data |
|
||||||
|
|
||||||
- `token_<telegram_user_id>.json` - Xiaomi auth token, secret file;
|
Paths to the database and status files are defined in `compose.yaml`. For running without Docker, refer to `secrets.env.example`.
|
||||||
- `miband_<telegram_user_id>.db` - SQLite database with health data;
|
|
||||||
- `status_<telegram_user_id>.json` - latest sync status;
|
|
||||||
- `fitness_bot_state.db` - Telegram menu state;
|
|
||||||
- `sync_<telegram_user_id>.lock` - sync lock file.
|
|
||||||
|
|
||||||
Do not commit `secrets.env`, `data/`, `*.db`, `token*.json` or `status*.json`. These files are already listed in `.gitignore`.
|
## Data Files
|
||||||
|
|
||||||
## Bot Commands
|
Runtime files are created in `./data`:
|
||||||
|
|
||||||
- `/start` - open the menu or start Xiaomi login.
|
| File | Content |
|
||||||
- `/sync` - run manual sync.
|
| --- | --- |
|
||||||
- `/status` - show local database status.
|
| `token_<id>.json` | Xiaomi auth token (**secret**) |
|
||||||
|
| `miband_<id>.db` | SQLite database with health data |
|
||||||
|
| `status_<id>.json` | Last sync status |
|
||||||
|
| `allowed_user.id` | ID of the bound owner |
|
||||||
|
| `fitness_bot_state.db` | Telegram menu internal state |
|
||||||
|
| `sync_<id>.lock` | Sync lock file |
|
||||||
|
|
||||||
Most actions are done with buttons in the Telegram menu.
|
`secrets.env`, `data/`, `*.db`, `token*.json`, and `status*.json`
|
||||||
|
are added to `.gitignore` — do not commit them.
|
||||||
|
|
||||||
## CSV Export
|
## Commands
|
||||||
|
|
||||||
The menu includes data export. The bot builds a ZIP with CSV tables and sends it through Telegram.
|
| Command | Action |
|
||||||
|
| --- | --- |
|
||||||
Remember: the ZIP contains health data and travels through Telegram infrastructure. Do not send it to shared chats or store it where other people can access it.
|
| `/start` | Open menu or start Xiaomi login flow |
|
||||||
|
| `/sync` | Start manual synchronization |
|
||||||
|
| `/status` | Show local database status |
|
||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 -m venv .venv
|
python3 -m venv .venv
|
||||||
.venv/bin/pip install -r requirements-dev.txt -e mi-fitness-python
|
.venv/bin/pip install -r requirements-dev.txt -e mi-fitness-python
|
||||||
.venv/bin/python -m py_compile fitness_bot.py miband_sync.py $(find miband_tracker -name '*.py' | sort)
|
.venv/bin/python -m py_compile fitness_bot.py miband_sync.py \
|
||||||
|
$(find miband_tracker -name '*.py' | sort)
|
||||||
.venv/bin/python -m pytest
|
.venv/bin/python -m pytest
|
||||||
.venv/bin/python -m pytest mi-fitness-python/tests/unit
|
.venv/bin/python -m pytest mi-fitness-python/tests/unit
|
||||||
.venv/bin/ruff check .
|
.venv/bin/ruff check .
|
||||||
.venv/bin/python -m pip check
|
.venv/bin/python -m pip check
|
||||||
```
|
```
|
||||||
|
|
||||||
Entrypoints are kept for Docker and local compatibility:
|
Entry points:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python -u miband_sync.py
|
python -u miband_sync.py # or: miband-sync
|
||||||
python -u fitness_bot.py
|
python -u fitness_bot.py # or: miband-fitness-bot
|
||||||
```
|
|
||||||
|
|
||||||
If installed as a Python package, console scripts are available:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
miband-sync
|
|
||||||
miband-fitness-bot
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**The bot does not respond.**
|
**The bot does not respond** — check `TELEGRAM_BOT_TOKEN`, check the logs, and make sure you were the first to send `/start` to the bot to bind it. If you need to reset the bound owner, simply delete the file `data/allowed_user.id` and send `/start` again.
|
||||||
Check `TELEGRAM_BOT_TOKEN`, `TELEGRAM_ALLOWED_USER_ID` and logs:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker compose logs -f fitness-bot
|
docker compose logs -f fitness-bot
|
||||||
```
|
```
|
||||||
|
|
||||||
**Sync says token is missing.**
|
**Token not found** — send `/start` and complete the Xiaomi login flow.
|
||||||
Open the bot in Telegram, send `/start` and complete the Xiaomi login flow.
|
|
||||||
|
|
||||||
**The token expired.**
|
**Token expired** — start a re-login from the menu; the old file
|
||||||
Run Xiaomi login again from the menu. You can remove the old token from `data/`.
|
can be deleted from `data/`.
|
||||||
|
|
||||||
**Some data is missing or there is no SpO2/sleep detail.**
|
**No SpO2 or sleep details** — make sure this data is visible
|
||||||
Check that Xiaomi Fitness itself shows that data. Some data depends on the band model, sharing settings and unofficial API availability.
|
in the Xiaomi Fitness app itself. Availability depends on the band model
|
||||||
|
and data sharing settings.
|
||||||
|
|
||||||
**Everything broke after a Xiaomi update.**
|
**Everything broke after a Xiaomi update** — this is an expected risk
|
||||||
That is an expected risk for a reverse-engineering project. Check issues/README and logs, then update the code or temporarily disable the affected feature.
|
when working with unofficial APIs. Check issues and logs, then
|
||||||
|
update the code or temporarily disable the problematic module.
|
||||||
|
|
||||||
## License And Vendored SDK
|
## Important: Reverse Engineering and Limitations
|
||||||
|
|
||||||
This project is licensed under GNU GPL v3.0 or later. The full license text is in [LICENSE](LICENSE).
|
`miband-bot` is an unofficial project, not affiliated with Xiaomi, Zepp,
|
||||||
|
Huami, or Telegram.
|
||||||
|
|
||||||
The `mi-fitness-python` SDK is kept in this repository as a vendored source copy and remains under its own GNU GPL v3.0 license: [mi-fitness-python/LICENSE](mi-fitness-python/LICENSE). Origin and update policy are documented in [VENDORED.md](VENDORED.md).
|
Data access is implemented via reverse engineering of closed APIs,
|
||||||
|
therefore:
|
||||||
|
|
||||||
|
- Xiaomi may change the API without warning;
|
||||||
|
- authorization or synchronization may temporarily stop working;
|
||||||
|
- use this project only with your own accounts and data;
|
||||||
|
- comply with applicable laws and services' terms of use;
|
||||||
|
- wristband data is not a medical opinion.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The project is distributed under the [GNU GPL v3.0 or later](LICENSE).
|
||||||
|
|
||||||
|
SDK `mi-fitness-python` is included as a vendored source copy under
|
||||||
|
[GNU GPL v3.0](mi-fitness-python/LICENSE). Details are in [VENDORED.md](VENDORED.md).
|
||||||
|
|||||||
+5
-1
@@ -4,7 +4,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from miband_tracker.bot.app import main
|
from miband_tracker.stdio import configure_utf8_stdio
|
||||||
|
|
||||||
|
configure_utf8_stdio()
|
||||||
|
|
||||||
|
from miband_tracker.bot.app import main # noqa: E402
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
# Отключаем показ прогресс-баров (скрывает спам при скачивании)
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
|
||||||
|
# Кодировка UTF-8 для корректного вывода русских символов
|
||||||
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
Write-Host "=== Установка miband-bot ===" -ForegroundColor Blue
|
||||||
|
|
||||||
|
function Test-ConfiguredInstall {
|
||||||
|
param([string]$ProjectPath)
|
||||||
|
|
||||||
|
$secretsPath = Join-Path $ProjectPath "secrets.env"
|
||||||
|
if (-not (Test-Path -LiteralPath $secretsPath)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return [bool](Select-String -LiteralPath $secretsPath -Pattern '^TELEGRAM_BOT_TOKEN=.+$' -Quiet)
|
||||||
|
} catch {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Проверяем, запущен ли скрипт в защищенной системной папке (например, System32 или C:\Windows)
|
||||||
|
if ($PWD.Path -like "*\system32*" -or $PWD.Path -eq $env:SystemRoot) {
|
||||||
|
Write-Host "Предупреждение: Вы находитесь в защищенной системной папке ($($PWD.Path))." -ForegroundColor Yellow
|
||||||
|
Write-Host "Чтобы избежать ошибок доступа, переключаемся в вашу домашнюю папку..." -ForegroundColor Yellow
|
||||||
|
Set-Location -Path $env:USERPROFILE
|
||||||
|
Write-Host "Новый путь установки: $($PWD.Path)\miband-bot`n" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
$INSTALL_DIR = "miband-bot"
|
||||||
|
$projectPath = Join-Path $PWD.Path $INSTALL_DIR
|
||||||
|
$autoStart = $false
|
||||||
|
|
||||||
|
# 1. Проверяем существование директории
|
||||||
|
if (Test-Path -Path $INSTALL_DIR) {
|
||||||
|
if (Test-ConfiguredInstall -ProjectPath $projectPath) {
|
||||||
|
Write-Host "Найдена настроенная установка. Обновляю файлы и запускаю бота..." -ForegroundColor Green
|
||||||
|
$autoStart = $true
|
||||||
|
} else {
|
||||||
|
Write-Host "Папка '$INSTALL_DIR' уже существует в этой директории." -ForegroundColor Yellow
|
||||||
|
$overwrite = Read-Host "Хотите перезаписать файлы проекта внутри нее? [Y/n]"
|
||||||
|
if ($overwrite -eq "") { $overwrite = "y" }
|
||||||
|
if ($overwrite -notmatch "^[Yy]$") {
|
||||||
|
Write-Host "Установка отменена." -ForegroundColor Red
|
||||||
|
Exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
New-Item -ItemType Directory -Force -Path $INSTALL_DIR | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Переходим в папку проекта
|
||||||
|
Set-Location -Path $INSTALL_DIR
|
||||||
|
|
||||||
|
# 2. Скачиваем ZIP-архив с GitHub
|
||||||
|
$zipUrl = "https://github.com/iAlexeyRu/miband-bot/archive/refs/heads/main.zip"
|
||||||
|
$tempZip = Join-Path $env:TEMP "miband-bot-temp.zip"
|
||||||
|
$unpackDir = "temp-unpack"
|
||||||
|
|
||||||
|
Write-Host "Загрузка последней версии проекта с GitHub..." -ForegroundColor Gray
|
||||||
|
if (Test-Path -LiteralPath $tempZip) {
|
||||||
|
Remove-Item -LiteralPath $tempZip -Force
|
||||||
|
}
|
||||||
|
Invoke-WebRequest -Uri $zipUrl -OutFile $tempZip
|
||||||
|
|
||||||
|
# 3. Распаковываем во временную папку
|
||||||
|
Write-Host "Распаковка файлов проекта..." -ForegroundColor Gray
|
||||||
|
if (Test-Path -LiteralPath $unpackDir) {
|
||||||
|
Remove-Item -LiteralPath $unpackDir -Recurse -Force
|
||||||
|
}
|
||||||
|
Expand-Archive -Path $tempZip -DestinationPath $unpackDir -Force
|
||||||
|
Remove-Item $tempZip
|
||||||
|
|
||||||
|
# 4. Копируем все файлы (включая скрытые) в корень папки установки
|
||||||
|
Get-ChildItem -Path "$unpackDir\miband-bot-main" -Force | ForEach-Object {
|
||||||
|
Copy-Item -Path $_.FullName -Destination "." -Recurse -Force
|
||||||
|
}
|
||||||
|
Remove-Item -Path $unpackDir -Recurse -Force
|
||||||
|
|
||||||
|
# 5. Запускаем интерактивный setup.bat
|
||||||
|
if ($autoStart) {
|
||||||
|
Write-Host "`n✓ Проект обновлён. Запускаю бота..." -ForegroundColor Green
|
||||||
|
$setupArgs = "/c setup.bat --start"
|
||||||
|
} else {
|
||||||
|
Write-Host "`n✓ Проект успешно загружен! Запускаем интерактивную настройку..." -ForegroundColor Green
|
||||||
|
$setupArgs = "/c setup.bat"
|
||||||
|
}
|
||||||
|
# Используем $PWD.Path вместо "." для передачи абсолютного корректного пути рабочей папки
|
||||||
|
Start-Process -FilePath "cmd.exe" -ArgumentList $setupArgs -WorkingDirectory $PWD.Path -NoNewWindow -Wait
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Скрипт бесшовной установки miband-bot для macOS / Linux
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Цвета для вывода в консоль
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
echo -e "${BLUE}=== Установка miband-bot ===${NC}"
|
||||||
|
|
||||||
|
# 1. Проверяем зависимости (curl или wget, и unzip)
|
||||||
|
if ! command -v curl &> /dev/null && ! command -v wget &> /dev/null; then
|
||||||
|
echo -e "${RED}Ошибка: Для загрузки требуется утилита curl или wget.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v unzip &> /dev/null; then
|
||||||
|
echo -e "${RED}Ошибка: Для установки требуется утилита unzip (установите ее через ваш менеджер пакетов).${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Создаем или проверяем директорию установки
|
||||||
|
INSTALL_DIR="miband-bot"
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
echo -e "${YELLOW}Папка '$INSTALL_DIR' уже существует в этой директории.${NC}"
|
||||||
|
read -p "Хотите перезаписать файлы проекта внутри нее? [Y/n]: " overwrite_confirm </dev/tty
|
||||||
|
if [ -z "$overwrite_confirm" ]; then
|
||||||
|
overwrite_confirm="y"
|
||||||
|
fi
|
||||||
|
if [[ ! "$overwrite_confirm" =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "${RED}Установка отменена.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
|
||||||
|
# 3. Скачиваем ZIP-архив с GitHub
|
||||||
|
echo "Загрузка последней версии проекта с GitHub..."
|
||||||
|
ZIP_URL="https://github.com/iAlexeyRu/miband-bot/archive/refs/heads/main.zip"
|
||||||
|
TEMP_ZIP="miband-bot-temp.zip"
|
||||||
|
|
||||||
|
if command -v curl &> /dev/null; then
|
||||||
|
curl -sSL -o "$TEMP_ZIP" "$ZIP_URL"
|
||||||
|
else
|
||||||
|
wget -q -O "$TEMP_ZIP" "$ZIP_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Распаковываем и очищаем временные файлы
|
||||||
|
echo "Распаковка файлов проекта..."
|
||||||
|
unzip -q -o "$TEMP_ZIP"
|
||||||
|
rm "$TEMP_ZIP"
|
||||||
|
|
||||||
|
# 5. Копируем файлы из вложенной папки и удаляем ее
|
||||||
|
cp -r miband-bot-main/. .
|
||||||
|
rm -rf miband-bot-main
|
||||||
|
|
||||||
|
# 6. Запускаем интерактивный setup.sh
|
||||||
|
chmod +x setup.sh
|
||||||
|
echo -e "${GREEN}✓ Проект успешно загружен! Запускаем интерактивную настройку...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Перенаправляем stdin на /dev/tty, чтобы интерактивные read-промпты работали корректно при запуске через пайп
|
||||||
|
./setup.sh </dev/tty
|
||||||
+6
-2
@@ -7,8 +7,12 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from miband_tracker.config import ConfigError, Settings
|
from miband_tracker.stdio import configure_utf8_stdio
|
||||||
from miband_tracker.sync import daemon_main, run_sync
|
|
||||||
|
configure_utf8_stdio()
|
||||||
|
|
||||||
|
from miband_tracker.config import ConfigError, Settings # noqa: E402
|
||||||
|
from miband_tracker.sync import daemon_main, run_sync # noqa: E402
|
||||||
|
|
||||||
__all__ = ["run_sync"]
|
__all__ = ["run_sync"]
|
||||||
|
|
||||||
|
|||||||
@@ -35,10 +35,12 @@ from miband_tracker.secure_files import save_auth_token
|
|||||||
from miband_tracker.sync import run_sync
|
from miband_tracker.sync import run_sync
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging — console shows only WARNING+ so users don't see debug spam.
|
||||||
|
# The mi-fitness vendored library uses loguru with Chinese debug messages;
|
||||||
|
# we suppress those to ERROR as well.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.WARNING,
|
||||||
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||||
)
|
)
|
||||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
@@ -46,6 +48,13 @@ logging.getLogger("telegram").setLevel(logging.WARNING)
|
|||||||
logging.getLogger("telegram.ext").setLevel(logging.WARNING)
|
logging.getLogger("telegram.ext").setLevel(logging.WARNING)
|
||||||
logger = logging.getLogger("fitness-bot")
|
logger = logging.getLogger("fitness-bot")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from loguru import logger as _loguru_logger
|
||||||
|
_loguru_logger.remove() # Remove loguru's default stderr handler
|
||||||
|
_loguru_logger.add(sys.stderr, level="ERROR", format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Config
|
# Config
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1499,10 +1508,11 @@ def main() -> None:
|
|||||||
ALLOWED_USER_ID = SETTINGS.telegram_allowed_user_id
|
ALLOWED_USER_ID = SETTINGS.telegram_allowed_user_id
|
||||||
if ALLOWED_USER_ID is not None:
|
if ALLOWED_USER_ID is not None:
|
||||||
DB_PATH = str(SETTINGS.user_db_path(ALLOWED_USER_ID))
|
DB_PATH = str(SETTINGS.user_db_path(ALLOWED_USER_ID))
|
||||||
logger.info("Стартую fitness-bot для пользователя: %s", ALLOWED_USER_ID)
|
print(f"Запуск бота для пользователя ID {ALLOWED_USER_ID}...")
|
||||||
else:
|
else:
|
||||||
DB_PATH = str(SETTINGS.db_path)
|
DB_PATH = str(SETTINGS.db_path)
|
||||||
logger.info("Стартую fitness-bot в режиме ожидания привязки владельца (первое входящее сообщение привяжет бота)...")
|
print("Бот запущен. Отправьте /start в Telegram чтобы привязать аккаунт.")
|
||||||
|
|
||||||
|
|
||||||
init_state_db()
|
init_state_db()
|
||||||
app = Application.builder().token(BOT_TOKEN).build()
|
app = Application.builder().token(BOT_TOKEN).build()
|
||||||
|
|||||||
@@ -8,6 +8,28 @@ from dataclasses import dataclass
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _load_local_env(filename: str = "secrets.env") -> None:
|
||||||
|
"""Load KEY=VALUE pairs from a local .env file into os.environ.
|
||||||
|
|
||||||
|
Only sets variables that are NOT already present in the environment
|
||||||
|
(explicit env always wins). Skips blank lines and comments (#).
|
||||||
|
"""
|
||||||
|
env_file = Path(filename)
|
||||||
|
if not env_file.exists():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
for raw_line in env_file.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
key, _, value = line.partition("=")
|
||||||
|
key = key.strip()
|
||||||
|
if key and key not in os.environ:
|
||||||
|
os.environ[key] = value.strip()
|
||||||
|
except Exception:
|
||||||
|
pass # Never crash on env-file read failure
|
||||||
|
|
||||||
|
|
||||||
class ConfigError(ValueError):
|
class ConfigError(ValueError):
|
||||||
"""Invalid runtime configuration."""
|
"""Invalid runtime configuration."""
|
||||||
|
|
||||||
@@ -44,7 +66,18 @@ class Settings:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_env(cls, *, require_bot: bool = False) -> Settings:
|
def from_env(cls, *, require_bot: bool = False) -> Settings:
|
||||||
data_dir = Path(os.environ.get("DATA_DIR", "/opt/miband-tracker/data"))
|
# Load local secrets.env (if present) before reading env vars.
|
||||||
|
# This makes Python the single source of truth on all platforms.
|
||||||
|
_load_local_env()
|
||||||
|
# Auto-detect local vs Docker mode: if DATA_DIR is not set, use ./data
|
||||||
|
# when running locally (secrets.env present or ./data already exists),
|
||||||
|
# otherwise fall back to the Docker default /opt/miband-tracker/data.
|
||||||
|
_default_data = (
|
||||||
|
"./data"
|
||||||
|
if (Path("secrets.env").exists() or Path("data").is_dir())
|
||||||
|
else "/opt/miband-tracker/data"
|
||||||
|
)
|
||||||
|
data_dir = Path(os.environ.get("DATA_DIR", _default_data))
|
||||||
allowed_user_id = parse_single_user_id(
|
allowed_user_id = parse_single_user_id(
|
||||||
os.environ.get("TELEGRAM_ALLOWED_USER_ID", ""), required=False
|
os.environ.get("TELEGRAM_ALLOWED_USER_ID", ""), required=False
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ def exclusive_file_lock(path: Path) -> Iterator[None]:
|
|||||||
lock_file.seek(0)
|
lock_file.seek(0)
|
||||||
# Блокируем первые 64 байта файла без ожидания (non-blocking)
|
# Блокируем первые 64 байта файла без ожидания (non-blocking)
|
||||||
msvcrt.locking(fd, msvcrt.LK_NBLCK, 64)
|
msvcrt.locking(fd, msvcrt.LK_NBLCK, 64)
|
||||||
except (OSError, IOError) as exc:
|
except OSError as exc:
|
||||||
raise LockUnavailable(f"Lock is already held: {path}") from exc
|
raise LockUnavailable(f"Lock is already held: {path}") from exc
|
||||||
else:
|
else:
|
||||||
# Резервный вариант, если блокировки недоступны на платформе
|
# Резервный вариант, если блокировки недоступны на платформе
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Alexey
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import TextIO
|
||||||
|
|
||||||
|
|
||||||
|
def configure_utf8_stdio() -> None:
|
||||||
|
os.environ.setdefault("PYTHONUTF8", "1")
|
||||||
|
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
|
||||||
|
|
||||||
|
for stream in (sys.stdout, sys.stderr):
|
||||||
|
reconfigure = getattr(stream, "reconfigure", None)
|
||||||
|
if reconfigure is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
reconfigure(encoding="utf-8", errors="backslashreplace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def safe_print(text: str, *, file: TextIO | None = None, flush: bool = False) -> None:
|
||||||
|
stream = file or sys.stdout
|
||||||
|
try:
|
||||||
|
print(text, file=stream, flush=flush)
|
||||||
|
return
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
encoding = getattr(stream, "encoding", None) or "utf-8"
|
||||||
|
fallback = text.encode(encoding, errors="backslashreplace").decode(encoding, errors="replace")
|
||||||
|
print(fallback, file=stream, flush=flush)
|
||||||
@@ -17,6 +17,7 @@ from .config import ConfigError, Settings
|
|||||||
from .fds import download_and_decrypt_sleep_details, parse_all_day_sleep_bytes
|
from .fds import download_and_decrypt_sleep_details, parse_all_day_sleep_bytes
|
||||||
from .lock import LockUnavailable, exclusive_file_lock
|
from .lock import LockUnavailable, exclusive_file_lock
|
||||||
from .secure_files import save_auth_token, write_json_atomic, write_secret_json
|
from .secure_files import save_auth_token, write_json_atomic, write_secret_json
|
||||||
|
from .stdio import safe_print
|
||||||
from .storage import init_health_db, sqlite_conn
|
from .storage import init_health_db, sqlite_conn
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ class SyncResult:
|
|||||||
|
|
||||||
def log(message: str) -> None:
|
def log(message: str) -> None:
|
||||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
print(f"[{now}] {message}", flush=True)
|
safe_print(f"[{now}] {message}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
def format_epoch(epoch: int | float | None) -> str | None:
|
def format_epoch(epoch: int | float | None) -> str | None:
|
||||||
@@ -364,24 +365,24 @@ def _write_status_file(status_path: Path, latest_steps, latest_heart_rate, lates
|
|||||||
async def daemon_main(settings: Settings | None = None) -> int:
|
async def daemon_main(settings: Settings | None = None) -> int:
|
||||||
settings = settings or Settings.from_env()
|
settings = settings or Settings.from_env()
|
||||||
if settings.sync_interval <= 0:
|
if settings.sync_interval <= 0:
|
||||||
log("Running in one-shot mode.")
|
|
||||||
result = await run_sync(settings=settings)
|
result = await run_sync(settings=settings)
|
||||||
return 0 if result.success else 1
|
return 0 if result.success else 1
|
||||||
|
|
||||||
log(f"Running in daemon mode. Sync interval: {settings.sync_interval} seconds.")
|
_waiting_logged = False
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
current_settings = Settings.from_env()
|
current_settings = Settings.from_env()
|
||||||
if current_settings.telegram_allowed_user_id is None:
|
if current_settings.telegram_allowed_user_id is None:
|
||||||
log("Waiting for allowed user ID to be registered via Telegram bot...")
|
if not _waiting_logged:
|
||||||
|
log("Синхронизатор ожидает привязки аккаунта через Telegram (/start)...")
|
||||||
|
_waiting_logged = True
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
_waiting_logged = False # Reset so we log again if user unregisters
|
||||||
await run_sync(settings=current_settings)
|
await run_sync(settings=current_settings)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log(f"Unhandled error in main loop: {exc}")
|
log(f"Unhandled error in main loop: {exc}")
|
||||||
|
|
||||||
# Load settings again to catch any runtime changes in sync interval
|
|
||||||
interval = Settings.from_env().sync_interval
|
interval = Settings.from_env().sync_interval
|
||||||
log(f"Sleeping for {interval} seconds...")
|
|
||||||
await asyncio.sleep(interval)
|
await asyncio.sleep(interval)
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ python-telegram-bot==21.6
|
|||||||
qrcode==8.0
|
qrcode==8.0
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
tenacity==9.1.2
|
tenacity==9.1.2
|
||||||
|
tzdata
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Alexey
|
||||||
|
"""
|
||||||
|
Local launcher: runs miband_sync and fitness_bot in parallel,
|
||||||
|
redirecting their output to data/sync.log and data/bot.log.
|
||||||
|
Handles Ctrl+C and window close gracefully.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from contextlib import suppress
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from miband_tracker.lock import LockUnavailable, exclusive_file_lock
|
||||||
|
from miband_tracker.stdio import configure_utf8_stdio, safe_print
|
||||||
|
|
||||||
|
configure_utf8_stdio()
|
||||||
|
|
||||||
|
|
||||||
|
def python_env() -> dict[str, str]:
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["PYTHONUTF8"] = "1"
|
||||||
|
env["PYTHONIOENCODING"] = "utf-8"
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def stop_processes(procs: list[subprocess.Popen[bytes]]) -> None:
|
||||||
|
for proc in procs:
|
||||||
|
if proc.poll() is None:
|
||||||
|
proc.terminate()
|
||||||
|
|
||||||
|
for proc in procs:
|
||||||
|
if proc.poll() is None:
|
||||||
|
with suppress(subprocess.TimeoutExpired):
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
|
||||||
|
for proc in procs:
|
||||||
|
if proc.poll() is None:
|
||||||
|
proc.kill()
|
||||||
|
|
||||||
|
|
||||||
|
def run_processes(data_dir: Path) -> int:
|
||||||
|
procs: list[subprocess.Popen[bytes]] = []
|
||||||
|
exit_code = 0
|
||||||
|
child_env = python_env()
|
||||||
|
|
||||||
|
with (data_dir / "sync.log").open("w", encoding="utf-8") as sync_log, (
|
||||||
|
data_dir / "bot.log"
|
||||||
|
).open("w", encoding="utf-8") as bot_log:
|
||||||
|
try:
|
||||||
|
sync_proc = subprocess.Popen(
|
||||||
|
[sys.executable, "-u", "miband_sync.py"],
|
||||||
|
stdout=sync_log,
|
||||||
|
stderr=sync_log,
|
||||||
|
env=child_env,
|
||||||
|
)
|
||||||
|
procs.append(sync_proc)
|
||||||
|
|
||||||
|
bot_proc = subprocess.Popen(
|
||||||
|
[sys.executable, "-u", "fitness_bot.py"],
|
||||||
|
stdout=bot_log,
|
||||||
|
stderr=bot_log,
|
||||||
|
env=child_env,
|
||||||
|
)
|
||||||
|
procs.append(bot_proc)
|
||||||
|
|
||||||
|
exit_code = bot_proc.wait()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
exit_code = 130
|
||||||
|
finally:
|
||||||
|
stop_processes(procs)
|
||||||
|
|
||||||
|
return exit_code
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
data_dir = Path("data")
|
||||||
|
data_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with exclusive_file_lock(data_dir / "run.lock"):
|
||||||
|
exit_code = run_processes(data_dir)
|
||||||
|
except LockUnavailable:
|
||||||
|
safe_print("miband-bot уже запущен. Закройте старое окно перед повторным запуском.", flush=True)
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
raise SystemExit(exit_code)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,259 +1,167 @@
|
|||||||
@echo off
|
@echo off
|
||||||
:: ==============================================================================
|
|
||||||
:: miband-bot Smart Setup Script for Windows (100% Flat robust version)
|
|
||||||
:: ==============================================================================
|
|
||||||
chcp 65001 > nul
|
chcp 65001 > nul
|
||||||
|
set AUTO_START=0
|
||||||
|
if /i "%~1"=="--start" set AUTO_START=1
|
||||||
|
if /i "%MIBAND_BOT_AUTO_START%"=="1" set AUTO_START=1
|
||||||
cls
|
cls
|
||||||
|
|
||||||
echo ======================================================================
|
|
||||||
echo 🚀 УМНЫЙ ЗАПУСК И НАСТРОЙКА miband-bot 🚀
|
|
||||||
echo ======================================================================
|
|
||||||
echo Этот скрипт поможет вам быстро развернуть личного Telegram-бота
|
|
||||||
echo для сбора данных Xiaomi Fitness / Mi Band.
|
|
||||||
echo.
|
echo.
|
||||||
|
echo miband-bot
|
||||||
:: ------------------------------------------------------------------------------
|
|
||||||
:: Шаг 1. Выбор метода установки
|
|
||||||
:: ------------------------------------------------------------------------------
|
|
||||||
echo Как вы хотите запустить бота?
|
|
||||||
echo 1] Напрямую на Python (Рекомендуется. Потребляет минимум памяти)
|
|
||||||
echo 2] В Docker контейнерах (Всё работает в фоне)
|
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
:CHOOSE_MODE
|
:CHOOSE_MODE
|
||||||
|
if "%AUTO_START%"=="1" if exist secrets.env goto MODE_PYTHON
|
||||||
set install_mode=1
|
set install_mode=1
|
||||||
set /p install_mode="Выберите вариант (1 или 2) [1]: "
|
set /p install_mode=" [1] Python (рек.) [2] Docker Выбор [1]: "
|
||||||
if "%install_mode%"=="1" goto MODE_PYTHON
|
if "%install_mode%"=="1" goto MODE_PYTHON
|
||||||
if "%install_mode%"=="2" goto MODE_DOCKER
|
if "%install_mode%"=="2" goto MODE_DOCKER
|
||||||
echo Неверный ввод. Пожалуйста, введите 1 или 2.
|
|
||||||
echo.
|
|
||||||
goto CHOOSE_MODE
|
goto CHOOSE_MODE
|
||||||
|
|
||||||
:: ------------------------------------------------------------------------------
|
|
||||||
:: Шаг 2. Проверка требований
|
|
||||||
:: ------------------------------------------------------------------------------
|
|
||||||
:MODE_DOCKER
|
:MODE_DOCKER
|
||||||
echo.
|
|
||||||
echo [1/3] Проверка окружения Docker...
|
|
||||||
where docker >nul 2>nul
|
where docker >nul 2>nul
|
||||||
if errorlevel 1 (
|
if errorlevel 1 ( echo ! Docker не найден. Установите Docker Desktop. & pause & exit /b 1 )
|
||||||
echo ❌ Docker CLI не найден. Пожалуйста, установите Docker Desktop или настройте WSL 2.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
docker info >nul 2>nul
|
docker info >nul 2>nul
|
||||||
if errorlevel 1 (
|
if errorlevel 1 ( echo ! Docker не запущен. Запустите Docker Desktop. & pause & exit /b 1 )
|
||||||
echo ❌ Демон Docker не запущен. Пожалуйста, запустите Docker и перезапустите скрипт.
|
echo OK Docker готов
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
echo ✓ Движок Docker активен и готов к работе!
|
|
||||||
set DOCKER_ACTIVE=1
|
set DOCKER_ACTIVE=1
|
||||||
goto SETUP_ENV
|
goto SETUP_ENV
|
||||||
|
|
||||||
:MODE_PYTHON
|
:MODE_PYTHON
|
||||||
echo.
|
|
||||||
echo [1/3] Проверка окружения Python...
|
|
||||||
python --version >nul 2>nul
|
python --version >nul 2>nul
|
||||||
if not errorlevel 1 goto PYTHON_OK
|
if not errorlevel 1 goto PYTHON_OK
|
||||||
|
|
||||||
echo ❌ Python не найден или не настроен в вашей системе.
|
echo ! Python не найден: https://www.python.org/downloads/
|
||||||
echo Вы можете скачать его вручную по ссылке: https://www.python.org/downloads/
|
|
||||||
echo.
|
echo.
|
||||||
set install_python=y
|
set install_python=y
|
||||||
set /p install_python="Хотите, чтобы я автоматически скачал и установил Python 3.11? [Y/n]: "
|
set /p install_python=" Установить Python 3.11 автоматически? [Y/n]: "
|
||||||
if /i not "%install_python%"=="y" (
|
if /i not "%install_python%"=="y" ( echo Установите Python вручную и повторите. & pause & exit /b 1 )
|
||||||
echo Установка отменена. Пожалуйста, установите Python вручную.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
echo Скачивание Python 3.11.9...
|
||||||
echo Скачивание установщика Python 3.11.9...
|
powershell -Command "$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri 'https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe' -OutFile 'python_installer.exe'"
|
||||||
powershell -Command "Invoke-WebRequest -Uri 'https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe' -OutFile 'python_installer.exe'"
|
echo Установка...
|
||||||
echo Установка Python (это займет около минуты, пожалуйста, подождите)...
|
|
||||||
python_installer.exe /quiet InstallAllUsers=0 PrependPath=1 Include_test=0
|
python_installer.exe /quiet InstallAllUsers=0 PrependPath=1 Include_test=0
|
||||||
del python_installer.exe
|
del python_installer.exe
|
||||||
|
|
||||||
:: Добавляем установленный Python в PATH текущей сессии
|
|
||||||
set "PATH=%LocalAppData%\Programs\Python\Python311;%LocalAppData%\Programs\Python\Python311\Scripts;%PATH%"
|
set "PATH=%LocalAppData%\Programs\Python\Python311;%LocalAppData%\Programs\Python\Python311\Scripts;%PATH%"
|
||||||
|
|
||||||
python --version >nul 2>nul
|
python --version >nul 2>nul
|
||||||
if errorlevel 1 (
|
if errorlevel 1 ( echo ! Не удалось установить Python. & pause & exit /b 1 )
|
||||||
echo ❌ Не удалось автоматически установить Python или добавить его в PATH.
|
|
||||||
echo Пожалуйста, установите Python вручную по ссылке: https://www.python.org/downloads/
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
:PYTHON_OK
|
:PYTHON_OK
|
||||||
echo ✓ Python найден!
|
echo OK Python готов
|
||||||
set DOCKER_ACTIVE=0
|
set DOCKER_ACTIVE=0
|
||||||
goto SETUP_ENV
|
goto SETUP_ENV
|
||||||
|
|
||||||
:: ------------------------------------------------------------------------------
|
|
||||||
:: Шаг 3. Настройка secrets.env
|
|
||||||
:: ------------------------------------------------------------------------------
|
|
||||||
:SETUP_ENV
|
:SETUP_ENV
|
||||||
echo.
|
echo.
|
||||||
echo [2/3] Конфигурация параметров бота (secrets.env)...
|
|
||||||
|
|
||||||
if not exist secrets.env goto INPUT_ENV_VALUES
|
if not exist secrets.env goto INPUT_ENV_VALUES
|
||||||
echo Файл конфигурации secrets.env уже существует.
|
if "%AUTO_START%"=="1" goto LAUNCH_PHASE
|
||||||
set overwrite_env=n
|
set overwrite_env=n
|
||||||
set /p overwrite_env="Хотите перезаписать его и настроить заново? [y/N]: "
|
set /p overwrite_env=" Конфигурация уже есть. Перенастроить? [y/N]: "
|
||||||
if /i not "%overwrite_env%"=="y" (
|
if /i not "%overwrite_env%"=="y" goto LAUNCH_PHASE
|
||||||
echo ✓ Сохраняем существующий secrets.env.
|
|
||||||
goto LAUNCH_PHASE
|
|
||||||
)
|
|
||||||
|
|
||||||
:INPUT_ENV_VALUES
|
:INPUT_ENV_VALUES
|
||||||
echo.
|
echo.
|
||||||
echo Сейчас мы настроим основные параметры безопасности.
|
echo Токен бота от @BotFather в Telegram:
|
||||||
echo.
|
|
||||||
|
|
||||||
:INPUT_TOKEN
|
:INPUT_TOKEN
|
||||||
echo 🔑 Шаг 1. Получение Telegram Bot Token
|
set bot_token=
|
||||||
echo Для работы бота необходим токен. Его можно получить бесплатно за 1 минуту:
|
set /p bot_token=" Токен: "
|
||||||
echo 1. Откройте Telegram и перейдите к официальному боту @BotFather.
|
if "%bot_token%"=="" goto TOKEN_EMPTY
|
||||||
echo 2. Отправьте ему команду /newbot.
|
echo %bot_token% | find ":" >nul 2>nul
|
||||||
echo 3. Введите название для бота (например, Мой Mi Band Бот).
|
if errorlevel 1 goto TOKEN_INVALID
|
||||||
echo 4. Введите уникальное имя пользователя (username) на английском,
|
goto TOKEN_OK
|
||||||
echo заканчивающееся на bot (например, my_miband_sync_bot).
|
|
||||||
echo 5. Скопируйте полученный токен (выглядит как 123456789:ABC-DEF...).
|
|
||||||
echo.
|
|
||||||
set /p bot_token="Вставьте ваш Telegram Bot Token: "
|
|
||||||
if "%bot_token%"=="" (
|
|
||||||
echo Ошибка: Токен не может быть пустым.
|
|
||||||
echo.
|
|
||||||
goto INPUT_TOKEN
|
|
||||||
)
|
|
||||||
echo %bot_token% | findstr /c:":" >nul
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo Ошибка: Токен должен содержать символ ":"
|
|
||||||
echo.
|
|
||||||
goto INPUT_TOKEN
|
|
||||||
)
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo 🔒 Шаг 2. Автоматическая привязка владельца (Whitelist ID)
|
:TOKEN_EMPTY
|
||||||
echo Вам НЕ НУЖНО вручную искать и вводить ваш Telegram User ID!
|
echo Токен не может быть пустым.
|
||||||
echo Сразу после запуска бота откройте его в Telegram и отправьте команду /start.
|
goto INPUT_TOKEN
|
||||||
echo Бот автоматически распознает ваш ID, запишет его в белый список
|
|
||||||
echo и заблокирует доступ для всех остальных пользователей.
|
|
||||||
echo.
|
|
||||||
|
|
||||||
:: Создание secrets.env (запись построчно во избежание багов парсера скобок)
|
:TOKEN_INVALID
|
||||||
echo # ============================================================================== > secrets.env
|
echo Неверный формат - токен должен содержать двоеточие.
|
||||||
echo # Конфигурация secrets.env для miband-bot >> secrets.env
|
goto INPUT_TOKEN
|
||||||
echo # ============================================================================== >> secrets.env
|
|
||||||
echo. >> secrets.env
|
|
||||||
echo # Токен вашего Telegram-бота (полученный от @BotFather) >> secrets.env
|
|
||||||
echo TELEGRAM_BOT_TOKEN=%bot_token% >> secrets.env
|
|
||||||
echo. >> secrets.env
|
|
||||||
echo # Единственный разрешенный Telegram User ID (для безопасности данных) >> secrets.env
|
|
||||||
echo # Оставьте пустым - бот автоматически привяжется к первому, кто напишет /start! >> secrets.env
|
|
||||||
echo TELEGRAM_ALLOWED_USER_ID= >> secrets.env
|
|
||||||
echo. >> secrets.env
|
|
||||||
echo # Интервал фоновой синхронизации данных из облака Xiaomi Fitness (в секундах) >> secrets.env
|
|
||||||
echo SYNC_INTERVAL=900 >> secrets.env
|
|
||||||
echo. >> secrets.env
|
|
||||||
echo # Глубина запроса при автоматической синхронизации (в днях) >> secrets.env
|
|
||||||
echo QUERY_DURATION=2 >> secrets.env
|
|
||||||
echo. >> secrets.env
|
|
||||||
echo # Загружать детальные ночные данные о сне (FDS) >> secrets.env
|
|
||||||
echo ENABLE_FDS_SLEEP_DETAILS=true >> secrets.env
|
|
||||||
|
|
||||||
echo ✓ Файл secrets.env успешно создан!
|
:TOKEN_OK
|
||||||
echo.
|
(
|
||||||
|
echo # miband-bot config
|
||||||
|
echo TELEGRAM_BOT_TOKEN=%bot_token%
|
||||||
|
echo TELEGRAM_ALLOWED_USER_ID=
|
||||||
|
echo SYNC_INTERVAL=900
|
||||||
|
echo QUERY_DURATION=2
|
||||||
|
echo ENABLE_FDS_SLEEP_DETAILS=true
|
||||||
|
) > secrets.env
|
||||||
|
echo OK Конфигурация сохранена
|
||||||
|
|
||||||
:: ------------------------------------------------------------------------------
|
|
||||||
:: Шаг 4. Установка зависимостей и запуск
|
|
||||||
:: ------------------------------------------------------------------------------
|
|
||||||
:LAUNCH_PHASE
|
:LAUNCH_PHASE
|
||||||
echo [3/3] Подготовка среды выполнения...
|
echo.
|
||||||
|
|
||||||
if not "%DOCKER_ACTIVE%"=="1" goto PYTHON_LAUNCH
|
if not "%DOCKER_ACTIVE%"=="1" goto PYTHON_LAUNCH
|
||||||
|
|
||||||
:DOCKER_LAUNCH
|
:DOCKER_LAUNCH
|
||||||
set launch_now=y
|
set launch_now=y
|
||||||
set /p launch_now="Хотите запустить miband-bot в Docker прямо сейчас? [Y/n]: "
|
set /p launch_now=" Запустить в Docker сейчас? [Y/n]: "
|
||||||
if /i not "%launch_now%"=="y" (
|
if /i not "%launch_now%"=="y" ( echo Запуск вручную: docker compose up -d --build & goto END_LAUNCH )
|
||||||
echo.
|
|
||||||
echo Бот настроен, но не запущен.
|
|
||||||
echo Запустите вручную командой: docker compose up -d --build
|
|
||||||
goto END_LAUNCH
|
|
||||||
)
|
|
||||||
echo Сборка и запуск контейнеров...
|
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
if not errorlevel 1 (
|
if not errorlevel 1 (
|
||||||
echo.
|
echo.
|
||||||
echo ======================================================================
|
echo OK Бот запущен в Docker. Отправьте /start в Telegram.
|
||||||
echo 🎉 miband-bot УСПЕШНО НАСТРОЕН И ЗАПУЩЕН В DOCKER! 🎉
|
echo Логи: docker compose logs -f fitness-bot
|
||||||
echo ======================================================================
|
|
||||||
echo Теперь откройте бота в Telegram и отправьте команду /start.
|
|
||||||
echo.
|
|
||||||
echo Логи: docker compose logs -f fitness-bot
|
|
||||||
echo Стоп: docker compose down
|
|
||||||
echo ======================================================================
|
|
||||||
) else (
|
) else (
|
||||||
echo Ошибка при сборке или запуске Docker Compose.
|
echo ! Ошибка запуска Docker.
|
||||||
)
|
)
|
||||||
goto END_LAUNCH
|
goto END_LAUNCH
|
||||||
|
|
||||||
:PYTHON_LAUNCH
|
:PYTHON_LAUNCH
|
||||||
echo Создаем виртуальное окружение .venv и устанавливаем зависимости...
|
echo Подготовка...
|
||||||
if not exist .venv (
|
if not exist .venv ( python -m venv .venv >nul 2>nul )
|
||||||
python -m venv .venv
|
|
||||||
)
|
|
||||||
call .venv\Scripts\activate
|
call .venv\Scripts\activate
|
||||||
echo Установка библиотек...
|
pip install -r requirements.txt -e mi-fitness-python > pip_install.log 2>&1
|
||||||
pip install -r requirements.txt -e mi-fitness-python
|
if not errorlevel 1 (
|
||||||
|
del pip_install.log >nul 2>nul
|
||||||
|
echo OK Готово
|
||||||
|
) else (
|
||||||
|
echo ! Ошибка установки зависимостей:
|
||||||
|
type pip_install.log
|
||||||
|
del pip_install.log >nul 2>nul
|
||||||
|
pause & exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
:: Записываем рабочий запуск в run_local.bat построчно (полностью без скобочных блоков)
|
>run_local.bat echo @echo off
|
||||||
echo @echo off > run_local.bat
|
>>run_local.bat echo chcp 65001 ^> nul
|
||||||
echo chcp 65001 ^> nul >> run_local.bat
|
>>run_local.bat echo cd /d "%%~dp0"
|
||||||
echo cd /d "%%~dp0" >> run_local.bat
|
>>run_local.bat echo if not exist .venv ^(
|
||||||
echo if not exist .venv ^( >> run_local.bat
|
>>run_local.bat echo echo ! .venv не найдено. Запустите setup.bat заново.
|
||||||
echo echo Ошибка: Виртуальное окружение .venv не найдено. Запустите setup.bat сначала. >> run_local.bat
|
>>run_local.bat echo pause
|
||||||
echo pause >> run_local.bat
|
>>run_local.bat echo exit /b 1
|
||||||
echo exit /b 1 >> run_local.bat
|
>>run_local.bat echo ^)
|
||||||
echo ^) >> run_local.bat
|
>>run_local.bat echo if not exist run.py ^(
|
||||||
echo. >> run_local.bat
|
>>run_local.bat echo echo ! run.py не найден. Обновите проект и запустите setup.bat заново.
|
||||||
echo :: Загрузка переменных из secrets.env >> run_local.bat
|
>>run_local.bat echo pause
|
||||||
echo if exist secrets.env ^( >> run_local.bat
|
>>run_local.bat echo exit /b 1
|
||||||
echo for /f "usebackq delims=" %%%%x in ^("secrets.env"^) do ^( >> run_local.bat
|
>>run_local.bat echo ^)
|
||||||
echo echo %%%%x ^| findstr /r "^#" ^>nul >> run_local.bat
|
>>run_local.bat echo call .venv\Scripts\activate
|
||||||
echo if errorlevel 1 ^( >> run_local.bat
|
>>run_local.bat echo set PYTHONUTF8=1
|
||||||
echo set %%%%x >> run_local.bat
|
>>run_local.bat echo set PYTHONIOENCODING=utf-8
|
||||||
echo ^) >> run_local.bat
|
>>run_local.bat echo md data 2^>nul
|
||||||
echo ^) >> run_local.bat
|
>>run_local.bat echo cls
|
||||||
echo ^) >> run_local.bat
|
>>run_local.bat echo echo.
|
||||||
echo. >> run_local.bat
|
>>run_local.bat echo echo miband-bot
|
||||||
echo :: Настройка локальных путей к папке data >> run_local.bat
|
>>run_local.bat echo echo.
|
||||||
echo set DATA_DIR=.\data >> run_local.bat
|
>>run_local.bat echo echo OK Бот запущен. Не закрывайте это окно.
|
||||||
echo set DB_PATH=.\data\miband_%%TELEGRAM_ALLOWED_USER_ID%%.db >> run_local.bat
|
>>run_local.bat echo echo Логи: data\bot.log / data\sync.log
|
||||||
echo set STATUS_PATH=.\data\status_%%TELEGRAM_ALLOWED_USER_ID%%.json >> run_local.bat
|
>>run_local.bat echo echo.
|
||||||
echo set BOT_STATE_DB_PATH=.\data\fitness_bot_state.db >> run_local.bat
|
>>run_local.bat echo python -u run.py
|
||||||
echo. >> run_local.bat
|
>>run_local.bat echo echo.
|
||||||
echo call .venv\Scripts\activate >> run_local.bat
|
>>run_local.bat echo echo Бот остановлен. Можно закрыть окно.
|
||||||
echo === Запуск miband-bot локально ^(без Docker^) === >> run_local.bat
|
>>run_local.bat echo echo.
|
||||||
echo Для завершения работы закройте это окно консоли. >> run_local.bat
|
>>run_local.bat echo pause
|
||||||
echo. >> run_local.bat
|
|
||||||
echo Запуск синхронизатора и Telegram-бота... >> run_local.bat
|
if "%AUTO_START%"=="1" goto RUN_LOCAL_NOW
|
||||||
echo. >> run_local.bat
|
|
||||||
echo [Запуск фонового синхронизатора...] >> run_local.bat
|
|
||||||
echo start /b python -u miband_sync.py >> run_local.bat
|
|
||||||
echo [Запуск бота...] >> run_local.bat
|
|
||||||
echo python -u fitness_bot.py >> run_local.bat
|
|
||||||
|
|
||||||
echo ✓ Создан удобный скрипт запуска: run_local.bat
|
|
||||||
echo.
|
echo.
|
||||||
set launch_now=y
|
set launch_now=y
|
||||||
set /p launch_now="Хотите запустить бота локально прямо сейчас? [Y/n]: "
|
set /p launch_now=" Запустить бота сейчас? [Y/n]: "
|
||||||
if /i "%launch_now%"=="y" (
|
if /i "%launch_now%"=="y" ( call run_local.bat )
|
||||||
call run_local.bat
|
goto END_LAUNCH
|
||||||
)
|
|
||||||
|
:RUN_LOCAL_NOW
|
||||||
|
call run_local.bat
|
||||||
|
|
||||||
:END_LAUNCH
|
:END_LAUNCH
|
||||||
echo.
|
echo.
|
||||||
|
|||||||
@@ -1,319 +1,171 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# miband-bot setup for macOS / Linux
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# miband-bot Smart Setup Script for macOS / Linux
|
|
||||||
# ==============================================================================
|
|
||||||
|
|
||||||
# Цвета для вывода в консоль
|
|
||||||
RED='\033[0;31m'
|
|
||||||
# Приятный зеленый
|
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
# Насыщенный желтый
|
RED='\033[0;31m'
|
||||||
YELLOW='\033[1;33m'
|
NC='\033[0m'
|
||||||
# Глубокий синий
|
|
||||||
BLUE='\033[0;34m'
|
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
||||||
# Пурпурный
|
err() { echo -e " ${RED}❌${NC} $1"; }
|
||||||
MAGENTA='\033[0;35m'
|
|
||||||
# Серый / обычный
|
|
||||||
NC='\033[0;37m'
|
|
||||||
BOLD='\033[1m'
|
|
||||||
|
|
||||||
clear
|
clear
|
||||||
echo -e "${BLUE}${BOLD}======================================================================"
|
echo ""
|
||||||
echo -e " 🚀 УМНЫЙ ЗАПУСК И НАСТРОЙКА miband-bot 🚀"
|
echo " miband-bot"
|
||||||
echo -e "======================================================================${NC}"
|
|
||||||
echo -e "Этот скрипт поможет вам быстро развернуть личного Telegram-бота"
|
|
||||||
echo -e "для сбора данных Xiaomi Fitness / Mi Band."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Шаг 1. Выбор метода установки
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
echo -e "${BOLD}Как вы хотите запустить бота?${NC}"
|
|
||||||
echo -e " ${BOLD}1)${NC} ${GREEN}Напрямую на Python${NC} (Рекомендуется. Потребляет минимум памяти)"
|
|
||||||
echo -e " ${BOLD}2)${NC} ${MAGENTA}В Docker контейнерах${NC} (Всё работает в фоне, легко управлять)"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# --- Выбор режима ---
|
||||||
while true; do
|
while true; do
|
||||||
read -p "Выберите вариант (1 или 2) [1]: " install_mode
|
read -p " [1] Python (рек.) [2] Docker Выбор [1]: " install_mode
|
||||||
if [ -z "$install_mode" ]; then
|
install_mode="${install_mode:-1}"
|
||||||
install_mode="1"
|
[ "$install_mode" = "1" ] || [ "$install_mode" = "2" ] && break
|
||||||
fi
|
|
||||||
if [ "$install_mode" = "1" ] || [ "$install_mode" = "2" ]; then
|
|
||||||
break
|
|
||||||
else
|
|
||||||
echo -e "${RED}Неверный ввод. Пожалуйста, введите 1 или 2.${NC}"
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# --- Проверка требований ---
|
||||||
# Шаг 2. Проверка требований выбранного режима
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
if [ "$install_mode" = "2" ]; then
|
if [ "$install_mode" = "2" ]; then
|
||||||
echo -e "${BOLD}[1/3] Проверка окружения Docker...${NC}"
|
|
||||||
|
|
||||||
DOCKER_AVAILABLE=true
|
|
||||||
DOCKER_RUNNING=true
|
|
||||||
|
|
||||||
if ! command -v docker &> /dev/null; then
|
if ! command -v docker &> /dev/null; then
|
||||||
DOCKER_AVAILABLE=false
|
err "Docker не найден. Установите Docker Desktop или Colima (brew install colima)."
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$DOCKER_AVAILABLE" = true ]; then
|
|
||||||
if ! docker info &> /dev/null; then
|
|
||||||
DOCKER_RUNNING=false
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$DOCKER_AVAILABLE" = false ]; then
|
|
||||||
echo -e "${RED}❌ Docker CLI не найден в вашей системе.${NC}"
|
|
||||||
echo -e "Пожалуйста, установите Docker движок перед продолжением:"
|
|
||||||
echo -e " - ${BLUE}Linux:${NC} Установите 'docker' и 'docker-compose-plugin' через менеджер пакетов."
|
|
||||||
echo -e " - ${BLUE}macOS:${NC} Запустите легкий ${BOLD}Colima${NC} ('brew install colima') или Docker Desktop."
|
|
||||||
echo -e "Или вернитесь назад и выберите вариант запуска напрямую на Python."
|
|
||||||
exit 1
|
exit 1
|
||||||
elif [ "$DOCKER_RUNNING" = false ]; then
|
fi
|
||||||
echo -e "${RED}❌ Служба Docker установлена, но сейчас не запущена.${NC}"
|
if ! docker info &> /dev/null; then
|
||||||
echo -e "Пожалуйста, запустите Docker-демон (например, 'colima start' на Mac"
|
err "Docker не запущен. Запустите демон (colima start / Docker Desktop) и повторите."
|
||||||
echo -e "или 'sudo systemctl start docker' на Linux) и перезапустите скрипт."
|
|
||||||
exit 1
|
exit 1
|
||||||
|
fi
|
||||||
|
if docker compose version &> /dev/null; then
|
||||||
|
COMPOSE_CMD="docker compose"
|
||||||
|
elif command -v docker-compose &> /dev/null; then
|
||||||
|
COMPOSE_CMD="docker-compose"
|
||||||
else
|
else
|
||||||
echo -e "${GREEN}✓ Движок Docker активен и готов к работе!${NC}"
|
err "Плагин docker compose не найден."
|
||||||
if docker compose version &> /dev/null; then
|
exit 1
|
||||||
COMPOSE_CMD="docker compose"
|
|
||||||
elif command -v docker-compose &> /dev/null; then
|
|
||||||
COMPOSE_CMD="docker-compose"
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}⚠️ Команда 'docker compose' не найдена. Убедитесь, что плагин Compose установлен.${NC}"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
ok "Docker готов"
|
||||||
|
DOCKER_MODE=true
|
||||||
else
|
else
|
||||||
echo -e "${BOLD}[1/3] Проверка окружения Python...${NC}"
|
|
||||||
|
|
||||||
PYTHON_CMD=""
|
PYTHON_CMD=""
|
||||||
if command -v python3 &> /dev/null; then
|
for cmd in python3 python; do
|
||||||
PYTHON_CMD="python3"
|
if command -v "$cmd" &> /dev/null; then
|
||||||
elif command -v python &> /dev/null; then
|
ver=$("$cmd" -c 'import sys; print(sys.version_info[0])' 2>/dev/null)
|
||||||
# Проверяем версию python
|
[ "$ver" = "3" ] && PYTHON_CMD="$cmd" && break
|
||||||
py_ver=$(python -c 'import sys; print(sys.version_info[0])' 2>/dev/null)
|
|
||||||
if [ "$py_ver" = "3" ]; then
|
|
||||||
PYTHON_CMD="python"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$PYTHON_CMD" ]; then
|
|
||||||
echo -e "${RED}❌ Python 3 не найден в вашей системе.${NC}"
|
|
||||||
echo -e "Пожалуйста, установите Python версии 3.10 или выше."
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
py_full_ver=$($PYTHON_CMD -c 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")')
|
|
||||||
echo -e "${GREEN}✓ Найден Python $py_full_ver!${NC}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Шаг 3. Настройка secrets.env
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
echo -e "${BOLD}[2/3] Конфигурация параметров бота (secrets.env)...${NC}"
|
|
||||||
|
|
||||||
ENV_FILE="secrets.env"
|
|
||||||
SETUP_CONFIG=true
|
|
||||||
|
|
||||||
if [ -f "$ENV_FILE" ]; then
|
|
||||||
echo -e "${YELLOW}Файл конфигурации secrets.env уже существует.${NC}"
|
|
||||||
read -p "Хотите перезаписать его и настроить параметры заново? [y/N]: " overwrite_env
|
|
||||||
if [ -z "$overwrite_env" ]; then
|
|
||||||
overwrite_env="n"
|
|
||||||
fi
|
|
||||||
if [[ ! $overwrite_env =~ ^[Yy]$ ]]; then
|
|
||||||
echo -e "${GREEN}✓ Сохраняем существующий secrets.env.${NC}"
|
|
||||||
SETUP_CONFIG=false
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$SETUP_CONFIG" = true ]; then
|
|
||||||
echo -e "Сейчас мы настроим основные параметры безопасности."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Ввод токена бота с подробной инструкцией
|
|
||||||
while true; do
|
|
||||||
echo -e "🔑 ${BOLD}Шаг 1. Получение Telegram Bot Token${NC}"
|
|
||||||
echo -e "Для работы бота необходим токен. Его можно получить бесплатно за 1 минуту:"
|
|
||||||
echo -e " 1. Откройте Telegram и перейдите к официальному боту ${BLUE}@BotFather${NC}."
|
|
||||||
echo -e " 2. Отправьте ему команду ${BOLD}/newbot${NC}."
|
|
||||||
echo -e " 3. Введите название для бота (например, ${YELLOW}Мой Mi Band Бот${NC})."
|
|
||||||
echo -e " 4. Введите уникальное имя пользователя (username) на английском,"
|
|
||||||
echo -e " заканчивающееся на ${BOLD}bot${NC} (например, ${YELLOW}my_miband_sync_bot${NC})."
|
|
||||||
echo -e " 5. Скопируйте полученный токен (выглядит как ${BLUE}123456789:ABC-DEF...${NC})."
|
|
||||||
echo ""
|
|
||||||
read -p "Вставьте ваш Telegram Bot Token: " bot_token
|
|
||||||
|
|
||||||
if [ -n "$bot_token" ] && [[ "$bot_token" == *":"* ]]; then
|
|
||||||
break
|
|
||||||
else
|
|
||||||
echo -e "${RED}Ошибка: Токен должен быть непустым и содержать символ ':'${NC}\n"
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
echo ""
|
if [ -z "$PYTHON_CMD" ]; then
|
||||||
|
err "Python 3 не найден. Установите: https://www.python.org/downloads/"
|
||||||
echo -e "🔒 ${BOLD}Шаг 2. Автоматическая привязка владельца (Whitelist ID)${NC}"
|
|
||||||
echo -e "Вам ${BOLD}НЕ НУЖНО${NC} вручную искать и вводить ваш Telegram User ID!"
|
|
||||||
echo -e "Сразу после запуска бота откройте его в Telegram и отправьте команду ${BOLD}/start${NC}."
|
|
||||||
echo -e "Бот автоматически распознает ваш ID, запишет его в белый список"
|
|
||||||
echo -e "и заблокирует доступ для всех остальных пользователей."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Создание secrets.env
|
|
||||||
cat << EOF > "$ENV_FILE"
|
|
||||||
# ==============================================================================
|
|
||||||
# Конфигурация secrets.env для miband-bot
|
|
||||||
# ==============================================================================
|
|
||||||
|
|
||||||
# Токен вашего Telegram-бота (полученный от @BotFather)
|
|
||||||
TELEGRAM_BOT_TOKEN=$bot_token
|
|
||||||
|
|
||||||
# Единственный разрешенный Telegram User ID (для безопасности данных)
|
|
||||||
# Оставьте пустым - бот автоматически привяжется к первому, кто напишет /start!
|
|
||||||
TELEGRAM_ALLOWED_USER_ID=
|
|
||||||
|
|
||||||
# Интервал фоновой синхронизации данных из облака Xiaomi Fitness (в секундах)
|
|
||||||
SYNC_INTERVAL=900
|
|
||||||
|
|
||||||
# Глубина запроса при автоматической синхронизации (в днях)
|
|
||||||
QUERY_DURATION=2
|
|
||||||
|
|
||||||
# Загружать детальные ночные данные о сне (FDS)
|
|
||||||
ENABLE_FDS_SLEEP_DETAILS=true
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod 600 "$ENV_FILE" 2>/dev/null
|
|
||||||
echo -e "${GREEN}✓ Файл secrets.env успешно создан!${NC}"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Шаг 4. Установка зависимостей и запуск
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
echo -e "${BOLD}[3/3] Подготовка среды выполнения...${NC}"
|
|
||||||
|
|
||||||
if [ "$install_mode" = "2" ]; then
|
|
||||||
# Режим Docker
|
|
||||||
read -p "Хотите запустить miband-bot в Docker прямо сейчас? [Y/n]: " launch_now
|
|
||||||
if [ -z "$launch_now" ]; then
|
|
||||||
launch_now="y"
|
|
||||||
fi
|
|
||||||
if [[ $launch_now =~ ^[Yy]$ ]]; then
|
|
||||||
echo -e "${BLUE}Сборка и старт контейнеров в фоне...${NC}"
|
|
||||||
if $COMPOSE_CMD up -d --build; then
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}${BOLD}======================================================================"
|
|
||||||
echo -e " 🎉 miband-bot УСПЕШНО НАСТРОЕН И ЗАПУЩЕН В DOCKER! 🎉"
|
|
||||||
echo -e "======================================================================${NC}"
|
|
||||||
echo -e "Следующие шаги:"
|
|
||||||
echo -e "1. Откройте вашего бота в Telegram и отправьте ${BOLD}/start${NC}."
|
|
||||||
echo -e "2. Войдите в аккаунт Xiaomi по ссылке или QR-коду."
|
|
||||||
echo ""
|
|
||||||
echo -e "Команда для просмотра логов:"
|
|
||||||
echo -e " ${BLUE}$COMPOSE_CMD logs -f fitness-bot${NC}"
|
|
||||||
echo -e "Команда для остановки бота:"
|
|
||||||
echo -e " ${BLUE}$COMPOSE_CMD down${NC}"
|
|
||||||
echo "======================================================================"
|
|
||||||
else
|
|
||||||
echo -e "${RED}Ошибка при запуске Docker Compose.${NC}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}Бот настроен, но не запущен.${NC}"
|
|
||||||
echo -e "Запустите вручную командой: ${BLUE}$COMPOSE_CMD up -d --build${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
else
|
|
||||||
# Режим чистого Python
|
|
||||||
echo -e "Создаем виртуальное окружение .venv..."
|
|
||||||
|
|
||||||
if [ ! -d ".venv" ]; then
|
|
||||||
if ! $PYTHON_CMD -m venv .venv; then
|
|
||||||
echo -e "${RED}❌ Не удалось создать виртуальное окружение .venv.${NC}"
|
|
||||||
echo -e "В вашей системе (вероятно, Debian/Ubuntu) отсутствует пакет для виртуальных сред."
|
|
||||||
echo -e "Пожалуйста, установите его с помощью команды:"
|
|
||||||
echo -e " ${BOLD}sudo apt update && sudo apt install -y python3-venv${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
source .venv/bin/activate
|
|
||||||
echo -e "${BLUE}Установка библиотек из requirements.txt и mi-fitness-python...${NC}"
|
|
||||||
if pip install -r requirements.txt -e mi-fitness-python; then
|
|
||||||
echo -e "${GREEN}✓ Все зависимости успешно установлены!${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ Не удалось установить зависимости. Проверьте подключение к сети.${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
ok "Python готов ($PYTHON_CMD)"
|
||||||
# Создаем удобный скрипт для локального запуска
|
DOCKER_MODE=false
|
||||||
LAUNCHER_FILE="run_local.sh"
|
fi
|
||||||
cat << 'EOF' > "$LAUNCHER_FILE"
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
# ==============================================================================
|
|
||||||
# Скрипт локального запуска miband-bot (без Docker)
|
|
||||||
# ==============================================================================
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
|
# --- Конфигурация ---
|
||||||
|
echo ""
|
||||||
|
if [ -f secrets.env ]; then
|
||||||
|
read -p " Конфигурация уже есть. Перенастроить? [y/N]: " overwrite < /dev/tty
|
||||||
|
if [[ ! "$overwrite" =~ ^[Yy]$ ]]; then
|
||||||
|
: # skip to launch
|
||||||
|
else
|
||||||
|
rm -f secrets.env
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f secrets.env ]; then
|
||||||
|
echo ""
|
||||||
|
echo " Токен бота от @BotFather в Telegram:"
|
||||||
|
while true; do
|
||||||
|
read -p " > " bot_token < /dev/tty
|
||||||
|
[ -z "$bot_token" ] && echo " Токен не может быть пустым." && continue
|
||||||
|
[[ "$bot_token" == *":"* ]] && break
|
||||||
|
echo " Неверный формат (должен содержать ':')."
|
||||||
|
done
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# miband-bot"
|
||||||
|
echo "TELEGRAM_BOT_TOKEN=$bot_token"
|
||||||
|
echo "TELEGRAM_ALLOWED_USER_ID="
|
||||||
|
echo "SYNC_INTERVAL=900"
|
||||||
|
echo "QUERY_DURATION=2"
|
||||||
|
echo "ENABLE_FDS_SLEEP_DETAILS=true"
|
||||||
|
} > secrets.env
|
||||||
|
ok "Конфигурация сохранена"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Запуск ---
|
||||||
|
echo ""
|
||||||
|
if [ "$DOCKER_MODE" = true ]; then
|
||||||
|
read -p " Запустить в Docker сейчас? [Y/n]: " launch_now < /dev/tty
|
||||||
|
launch_now="${launch_now:-y}"
|
||||||
|
if [[ ! "$launch_now" =~ ^[Yy]$ ]]; then
|
||||||
|
echo " Запуск вручную: $COMPOSE_CMD up -d --build"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
$COMPOSE_CMD up -d --build
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
ok "Бот запущен в Docker. Отправьте /start в Telegram."
|
||||||
|
echo " Логи: $COMPOSE_CMD logs -f fitness-bot"
|
||||||
|
else
|
||||||
|
err "Ошибка запуска Docker."
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Python режим
|
||||||
|
echo " Подготовка..."
|
||||||
if [ ! -d ".venv" ]; then
|
if [ ! -d ".venv" ]; then
|
||||||
echo "Ошибка: Папка .venv не найдена. Запустите setup.sh заново."
|
if ! $PYTHON_CMD -m venv .venv; then
|
||||||
|
err "Не удалось создать .venv. На Debian/Ubuntu: sudo apt install -y python3-venv"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
source .venv/bin/activate
|
||||||
|
if pip install -r requirements.txt -e mi-fitness-python > pip_install.log 2>&1; then
|
||||||
|
rm -f pip_install.log
|
||||||
|
ok "Готово"
|
||||||
|
else
|
||||||
|
err "Ошибка установки зависимостей:"
|
||||||
|
cat pip_install.log
|
||||||
|
rm -f pip_install.log
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Загружаем переменные из secrets.env в окружение текущего процесса
|
# Генерируем run_local.sh — логи Python уходят в файлы
|
||||||
if [ -f secrets.env ]; then
|
cat << 'EOF' > run_local.sh
|
||||||
while IFS= read -r line || [ -n "$line" ]; do
|
#!/usr/bin/env bash
|
||||||
# Очищаем пробелы и переносы
|
# Запуск miband-bot (Python читает secrets.env автоматически)
|
||||||
line=$(echo "$line" | xargs 2>/dev/null || echo "$line")
|
cd "$(dirname "$0")"
|
||||||
# Игнорируем комментарии и пустые строки
|
|
||||||
if [[ ! "$line" =~ ^# ]] && [[ ! -z "$line" ]]; then
|
if [ ! -d ".venv" ]; then
|
||||||
export "$line"
|
echo " ❌ .venv не найдена. Запустите setup.sh заново."
|
||||||
fi
|
exit 1
|
||||||
done < secrets.env
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Настраиваем локальные пути к папке data (чтобы не писать в глобальные пути /opt)
|
if [ ! -f "run.py" ]; then
|
||||||
export DATA_DIR="./data"
|
echo " ❌ run.py не найден. Обновите проект и запустите setup.sh заново."
|
||||||
export DB_PATH="./data/miband_${TELEGRAM_ALLOWED_USER_ID}.db"
|
exit 1
|
||||||
export STATUS_PATH="./data/status_${TELEGRAM_ALLOWED_USER_ID}.json"
|
fi
|
||||||
export BOT_STATE_DB_PATH="./data/fitness_bot_state.db"
|
|
||||||
|
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
echo "=== Запуск miband-bot локально на Python ==="
|
mkdir -p data
|
||||||
echo "Нажмите Ctrl+C для завершения работы обоих процессов."
|
clear
|
||||||
|
echo ""
|
||||||
|
echo " miband-bot"
|
||||||
|
echo ""
|
||||||
|
echo " ✓ Бот запущен. Не закрывайте это окно."
|
||||||
|
echo " Логи: data/bot.log / data/sync.log"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# При завершении скрипта (Ctrl+C) убиваем все запущенные нами фоновые задачи
|
python -u run.py
|
||||||
trap "kill 0" EXIT
|
echo ""
|
||||||
|
echo " Бот остановлен. Можно закрыть окно."
|
||||||
# Запускаем фоновый синхронизатор и сам Telegram бот параллельно
|
echo ""
|
||||||
python -u miband_sync.py &
|
|
||||||
python -u fitness_bot.py &
|
|
||||||
|
|
||||||
# Ожидаем завершения
|
|
||||||
wait
|
|
||||||
EOF
|
EOF
|
||||||
chmod +x "$LAUNCHER_FILE"
|
chmod +x run_local.sh
|
||||||
echo -e "${GREEN}✓ Создан удобный скрипт запуска: ${BOLD}./run_local.sh${NC}"
|
ok "Создан скрипт запуска: ./run_local.sh"
|
||||||
echo ""
|
|
||||||
|
|
||||||
read -p "Хотите запустить бота локально прямо сейчас? [Y/n]: " launch_now
|
|
||||||
if [ -z "$launch_now" ]; then
|
|
||||||
launch_now="y"
|
|
||||||
fi
|
|
||||||
if [[ $launch_now =~ ^[Yy]$ ]]; then
|
|
||||||
./run_local.sh
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}Бот готов к запуску.${NC}"
|
|
||||||
echo -e "Запускайте его в любое время с помощью скрипта:"
|
|
||||||
echo -e " ${BLUE}./run_local.sh${NC}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
read -p " Запустить бота сейчас? [Y/n]: " launch_now < /dev/tty
|
||||||
|
launch_now="${launch_now:-y}"
|
||||||
|
if [[ "$launch_now" =~ ^[Yy]$ ]]; then
|
||||||
|
./run_local.sh
|
||||||
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Alexey
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
|
||||||
|
from miband_tracker.stdio import safe_print
|
||||||
|
|
||||||
|
|
||||||
|
def test_safe_print_does_not_crash_on_non_ascii_with_charmap_stream() -> None:
|
||||||
|
raw = io.BytesIO()
|
||||||
|
stream = io.TextIOWrapper(raw, encoding="cp1251", errors="strict")
|
||||||
|
|
||||||
|
safe_print("API 业务错误", file=stream, flush=True)
|
||||||
|
stream.flush()
|
||||||
|
|
||||||
|
assert b"API " in raw.getvalue()
|
||||||
|
assert b"\\u4e1a" in raw.getvalue()
|
||||||
Reference in New Issue
Block a user