mirror of
https://github.com/alexgetmancom/miband-bot.git
synced 2026-07-19 04:00:15 +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
|
||||
*.tar
|
||||
*.tar.gz
|
||||
|
||||
# Generated launchers
|
||||
run_local.sh
|
||||
run_local.bat
|
||||
|
||||
@@ -2,186 +2,195 @@
|
||||
|
||||
Русский | [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.
|
||||
- Запускает ручную синхронизацию кнопкой в меню.
|
||||
- Автоматически синхронизирует данные по расписанию.
|
||||
- Хранит историю в SQLite в папке `data/`.
|
||||
- Экспортирует накопленные таблицы в ZIP с CSV-файлами.
|
||||
- Работает через Docker Compose.
|
||||
- Пишет Xiaomi token атомарно с правами `0600`.
|
||||
|
||||
## Кому подходит
|
||||
|
||||
Проект подойдет, если вы:
|
||||
|
||||
- пользуетесь 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 без предупреждения;
|
||||
- вход или синхронизация могут временно перестать работать;
|
||||
- используйте проект только для своих аккаунтов и своих данных;
|
||||
- соблюдайте применимые законы и условия сервисов в вашей стране;
|
||||
- данные браслета не являются медицинским заключением.
|
||||
- Просмотр последних шагов, сна, пульса и SpO2 в Telegram.
|
||||
- Ручная и автоматическая синхронизация по расписанию.
|
||||
- Хранение истории в SQLite (`data/`).
|
||||
- Экспорт всех таблиц в ZIP с CSV-файлами прямо в чат.
|
||||
- Развёртывание через Docker Compose.
|
||||
- Атомарная запись Xiaomi-токена с правами `0600`.
|
||||
- Умная автопривязка к первому пользователю (whitelist).
|
||||
|
||||
## Как это работает
|
||||
|
||||
```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;
|
||||
- `fitness-bot` - отвечает в Telegram, показывает меню, запускает ручной sync и экспорт.
|
||||
- `tracker` — периодически синхронизирует данные из Xiaomi Fitness;
|
||||
- `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 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
|
||||
TELEGRAM_BOT_TOKEN=123456:replace-me
|
||||
TELEGRAM_ALLOWED_USER_ID=123456789
|
||||
```
|
||||
Установщик сам создаст папку `miband-bot`, загрузит и распакует файлы проекта, проверит окружение и запустит интерактивную настройку!
|
||||
Повторный запуск этой же PowerShell-команды в уже настроенной установке обновит файлы и сразу запустит бота без повторного ввода токена.
|
||||
|
||||
3. Запустите сервис:
|
||||
---
|
||||
|
||||
```sh
|
||||
docker compose up -d --build
|
||||
docker compose logs -f fitness-bot
|
||||
```
|
||||
### Способ 2: Запуск из скачанной папки
|
||||
|
||||
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
|
||||
SYNC_INTERVAL=900
|
||||
QUERY_DURATION=2
|
||||
ENABLE_FDS_SLEEP_DETAILS=true
|
||||
```
|
||||
| Переменная | По умолчанию | Описание |
|
||||
| -------------------------- | ------------ | --------------------------------------- |
|
||||
| `TELEGRAM_BOT_TOKEN` | — | Token Telegram-бота |
|
||||
| `TELEGRAM_ALLOWED_USER_ID` | — | Разрешённый user id (оставьте пустым для автопривязки) |
|
||||
| `SYNC_INTERVAL` | `900` | Интервал фоновой синхронизации, секунды |
|
||||
| `QUERY_DURATION` | `2` | Глубина запроса при sync, дней |
|
||||
| `ENABLE_FDS_SLEEP_DETAILS` | `true` | Загружать детальные ночные данные FDS |
|
||||
|
||||
- `TELEGRAM_BOT_TOKEN` - token вашего Telegram-бота.
|
||||
- `TELEGRAM_ALLOWED_USER_ID` - единственный Telegram user id, которому разрешен доступ.
|
||||
- `SYNC_INTERVAL` - интервал фоновой синхронизации в секундах. `900` = 15 минут.
|
||||
- `QUERY_DURATION` - сколько последних дней запрашивать при sync.
|
||||
- `ENABLE_FDS_SLEEP_DETAILS` - пробовать ли загружать детальные ночные данные FDS.
|
||||
Пути к базе и статусу заданы в `compose.yaml`. При запуске без Docker
|
||||
смотрите `secrets.env.example`.
|
||||
|
||||
Пути к базе и статусу уже заданы в `compose.yaml`. Если запускаете без Docker, смотрите `secrets.env.example`.
|
||||
|
||||
## Где лежат данные
|
||||
## Файлы данных
|
||||
|
||||
Runtime-файлы создаются в `./data`:
|
||||
|
||||
- `token_<telegram_user_id>.json` - Xiaomi auth token, секретный файл;
|
||||
- `miband_<telegram_user_id>.db` - SQLite-база с health-данными;
|
||||
- `status_<telegram_user_id>.json` - последний статус синхронизации;
|
||||
- `fitness_bot_state.db` - служебное состояние Telegram-меню;
|
||||
- `sync_<telegram_user_id>.lock` - lock-файл синхронизации.
|
||||
| Файл | Содержимое |
|
||||
| ---------------------- | --------------------------------- |
|
||||
| `token_<id>.json` | Xiaomi auth token (**секретный**) |
|
||||
| `miband_<id>.db` | SQLite-база с health-данными |
|
||||
| `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` - показать состояние локальной базы.
|
||||
|
||||
Основное управление происходит кнопками в Telegram-меню.
|
||||
|
||||
## Экспорт CSV
|
||||
|
||||
В меню есть экспорт данных. Бот собирает ZIP с CSV-таблицами и отправляет его в Telegram.
|
||||
|
||||
Помните: ZIP с health-данными уходит через инфраструктуру Telegram. Не отправляйте экспорт в чужие чаты и не храните его там, где доступ есть у других людей.
|
||||
| Команда | Действие |
|
||||
| --------- | ------------------------------------- |
|
||||
| `/start` | Открыть меню или начать вход в Xiaomi |
|
||||
| `/sync` | Запустить ручную синхронизацию |
|
||||
| `/status` | Показать состояние локальной базы |
|
||||
|
||||
## Локальная разработка
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
.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 mi-fitness-python/tests/unit
|
||||
.venv/bin/ruff check .
|
||||
.venv/bin/python -m pip check
|
||||
```
|
||||
|
||||
Entrypoints сохранены для Docker и локального запуска:
|
||||
Точки входа:
|
||||
|
||||
```sh
|
||||
python -u miband_sync.py
|
||||
python -u fitness_bot.py
|
||||
```
|
||||
|
||||
Если проект установлен как Python package, доступны console scripts:
|
||||
|
||||
```sh
|
||||
miband-sync
|
||||
miband-fitness-bot
|
||||
python -u miband_sync.py # или: miband-sync
|
||||
python -u fitness_bot.py # или: miband-fitness-bot
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Бот не отвечает.**
|
||||
Проверьте `TELEGRAM_BOT_TOKEN`, `TELEGRAM_ALLOWED_USER_ID` и логи:
|
||||
**Бот не отвечает** — проверьте `TELEGRAM_BOT_TOKEN`, логи, а также убедитесь, что вы первыми отправили `/start` боту для привязки. При необходимости сбросить привязанного владельца просто удалите файл `data/allowed_user.id` и отправьте `/start` снова.
|
||||
|
||||
```sh
|
||||
docker compose logs -f fitness-bot
|
||||
```
|
||||
|
||||
**Синхронизация пишет, что token не найден.**
|
||||
Откройте бота в Telegram, отправьте `/start` и пройдите Xiaomi login flow.
|
||||
**Token не найден** — отправьте `/start` и пройдите Xiaomi login flow.
|
||||
|
||||
**Token истек.**
|
||||
В меню запустите повторный вход в Xiaomi. Старый token можно удалить из `data/`.
|
||||
**Token истёк** — запустите повторный вход из меню; старый файл
|
||||
можно удалить из `data/`.
|
||||
|
||||
**Данных мало или нет SpO2/деталей сна.**
|
||||
Проверьте, что Xiaomi Fitness реально показывает эти данные. Часть данных зависит от модели браслета, настроек шаринга и доступности неофициального API.
|
||||
**Нет SpO2 или деталей сна** — убедитесь, что эти данные отображаются
|
||||
в самом приложении Xiaomi Fitness. Доступность зависит от модели
|
||||
браслета и настроек шаринга.
|
||||
|
||||
**После обновления Xiaomi все сломалось.**
|
||||
Это ожидаемый риск reverse-engineering проекта. Проверьте issues/README и логи, затем обновите код или временно отключите проблемную часть.
|
||||
**После обновления Xiaomi всё сломалось** — это ожидаемый риск
|
||||
при работе с неофициальным 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
|
||||
|
||||
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.
|
||||
- Runs manual sync from a Telegram button.
|
||||
- Syncs data automatically on a schedule.
|
||||
- Stores history in SQLite under `data/`.
|
||||
- Exports accumulated tables as a ZIP with CSV files.
|
||||
- Runs with Docker Compose.
|
||||
- Writes the Xiaomi token atomically with mode `0600`.
|
||||
- View recent steps, sleep, heart rate, and SpO2 in Telegram.
|
||||
- Manual and scheduled automatic synchronization.
|
||||
- History storage in SQLite (`data/`).
|
||||
- Export of all tables to a ZIP archive with CSV files directly into the chat.
|
||||
- Deployment via Docker Compose.
|
||||
- Atomic writing of the Xiaomi token with `0600` permissions.
|
||||
- Smart auto-binding to the first user (whitelist).
|
||||
|
||||
## Who It Is For
|
||||
|
||||
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
|
||||
## How it works
|
||||
|
||||
```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;
|
||||
- `fitness-bot` - responds in Telegram, shows the menu, starts manual sync and exports data.
|
||||
- `tracker` — periodically synchronizes data from Xiaomi Fitness;
|
||||
- `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
|
||||
|
||||
- A server or home machine with Docker and Docker Compose.
|
||||
- A Telegram bot token from [@BotFather](https://t.me/BotFather).
|
||||
- Your Telegram user id.
|
||||
- Docker and Docker Compose (or installed Python 3.10+).
|
||||
- Telegram bot token from [@BotFather](https://t.me/BotFather).
|
||||
- A Xiaomi account with Xiaomi Fitness data.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Copy the secrets template:
|
||||
### Method 1: Seamless One-Click Installation (Recommended)
|
||||
|
||||
```sh
|
||||
cp secrets.env.example secrets.env
|
||||
```
|
||||
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:
|
||||
|
||||
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
|
||||
TELEGRAM_BOT_TOKEN=123456:replace-me
|
||||
TELEGRAM_ALLOWED_USER_ID=123456789
|
||||
```
|
||||
The installer will automatically create a `miband-bot` directory, download and extract the project files, verify dependencies, and launch the interactive setup!
|
||||
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.
|
||||
|
||||
3. Start the service:
|
||||
---
|
||||
|
||||
```sh
|
||||
docker compose up -d --build
|
||||
docker compose logs -f fitness-bot
|
||||
```
|
||||
### Method 2: Launch from Downloaded Directory
|
||||
|
||||
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
|
||||
TELEGRAM_BOT_TOKEN=123456:replace-me
|
||||
TELEGRAM_ALLOWED_USER_ID=123456789
|
||||
SYNC_INTERVAL=900
|
||||
QUERY_DURATION=2
|
||||
ENABLE_FDS_SLEEP_DETAILS=true
|
||||
```
|
||||
### Method 3: Fully Manual Setup:
|
||||
|
||||
- `TELEGRAM_BOT_TOKEN` - your Telegram bot token.
|
||||
- `TELEGRAM_ALLOWED_USER_ID` - the single Telegram user id allowed to access the bot.
|
||||
- `SYNC_INTERVAL` - background sync interval in seconds. `900` = 15 minutes.
|
||||
- `QUERY_DURATION` - how many recent days to query on each sync.
|
||||
- `ENABLE_FDS_SLEEP_DETAILS` - whether to try fetching detailed FDS sleep data.
|
||||
1. Copy the configuration template:
|
||||
```sh
|
||||
cp secrets.env.example secrets.env
|
||||
```
|
||||
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;
|
||||
- `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.
|
||||
Paths to the database and status files are defined in `compose.yaml`. For running without Docker, refer to `secrets.env.example`.
|
||||
|
||||
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.
|
||||
- `/sync` - run manual sync.
|
||||
- `/status` - show local database status.
|
||||
| File | Content |
|
||||
| --- | --- |
|
||||
| `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.
|
||||
|
||||
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.
|
||||
| Command | Action |
|
||||
| --- | --- |
|
||||
| `/start` | Open menu or start Xiaomi login flow |
|
||||
| `/sync` | Start manual synchronization |
|
||||
| `/status` | Show local database status |
|
||||
|
||||
## Local Development
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
.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 mi-fitness-python/tests/unit
|
||||
.venv/bin/ruff check .
|
||||
.venv/bin/python -m pip check
|
||||
```
|
||||
|
||||
Entrypoints are kept for Docker and local compatibility:
|
||||
Entry points:
|
||||
|
||||
```sh
|
||||
python -u miband_sync.py
|
||||
python -u fitness_bot.py
|
||||
```
|
||||
|
||||
If installed as a Python package, console scripts are available:
|
||||
|
||||
```sh
|
||||
miband-sync
|
||||
miband-fitness-bot
|
||||
python -u miband_sync.py # or: miband-sync
|
||||
python -u fitness_bot.py # or: miband-fitness-bot
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**The bot does not respond.**
|
||||
Check `TELEGRAM_BOT_TOKEN`, `TELEGRAM_ALLOWED_USER_ID` and logs:
|
||||
**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.
|
||||
|
||||
```sh
|
||||
docker compose logs -f fitness-bot
|
||||
```
|
||||
|
||||
**Sync says token is missing.**
|
||||
Open the bot in Telegram, send `/start` and complete the Xiaomi login flow.
|
||||
**Token not found** — send `/start` and complete the Xiaomi login flow.
|
||||
|
||||
**The token expired.**
|
||||
Run Xiaomi login again from the menu. You can remove the old token from `data/`.
|
||||
**Token expired** — start a re-login from the menu; the old file
|
||||
can be deleted from `data/`.
|
||||
|
||||
**Some data is missing or there is no SpO2/sleep detail.**
|
||||
Check that Xiaomi Fitness itself shows that data. Some data depends on the band model, sharing settings and unofficial API availability.
|
||||
**No SpO2 or sleep details** — make sure this data is visible
|
||||
in the Xiaomi Fitness app itself. Availability depends on the band model
|
||||
and data sharing settings.
|
||||
|
||||
**Everything broke after a Xiaomi update.**
|
||||
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.
|
||||
**Everything broke after a Xiaomi update** — this is an expected risk
|
||||
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 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__":
|
||||
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 sys
|
||||
|
||||
from miband_tracker.config import ConfigError, Settings
|
||||
from miband_tracker.sync import daemon_main, run_sync
|
||||
from miband_tracker.stdio import configure_utf8_stdio
|
||||
|
||||
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"]
|
||||
|
||||
|
||||
@@ -35,10 +35,12 @@ from miband_tracker.secure_files import save_auth_token
|
||||
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(
|
||||
level=logging.INFO,
|
||||
level=logging.WARNING,
|
||||
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
@@ -46,6 +48,13 @@ logging.getLogger("telegram").setLevel(logging.WARNING)
|
||||
logging.getLogger("telegram.ext").setLevel(logging.WARNING)
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1499,10 +1508,11 @@ def main() -> None:
|
||||
ALLOWED_USER_ID = SETTINGS.telegram_allowed_user_id
|
||||
if ALLOWED_USER_ID is not None:
|
||||
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:
|
||||
DB_PATH = str(SETTINGS.db_path)
|
||||
logger.info("Стартую fitness-bot в режиме ожидания привязки владельца (первое входящее сообщение привяжет бота)...")
|
||||
print("Бот запущен. Отправьте /start в Telegram чтобы привязать аккаунт.")
|
||||
|
||||
|
||||
init_state_db()
|
||||
app = Application.builder().token(BOT_TOKEN).build()
|
||||
|
||||
@@ -8,6 +8,28 @@ from dataclasses import dataclass
|
||||
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):
|
||||
"""Invalid runtime configuration."""
|
||||
|
||||
@@ -44,7 +66,18 @@ class Settings:
|
||||
|
||||
@classmethod
|
||||
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(
|
||||
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)
|
||||
# Блокируем первые 64 байта файла без ожидания (non-blocking)
|
||||
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
|
||||
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 .lock import LockUnavailable, exclusive_file_lock
|
||||
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
|
||||
|
||||
|
||||
@@ -34,7 +35,7 @@ class SyncResult:
|
||||
|
||||
def log(message: str) -> None:
|
||||
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:
|
||||
@@ -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:
|
||||
settings = settings or Settings.from_env()
|
||||
if settings.sync_interval <= 0:
|
||||
log("Running in one-shot mode.")
|
||||
result = await run_sync(settings=settings)
|
||||
return 0 if result.success else 1
|
||||
|
||||
log(f"Running in daemon mode. Sync interval: {settings.sync_interval} seconds.")
|
||||
_waiting_logged = False
|
||||
while True:
|
||||
try:
|
||||
current_settings = Settings.from_env()
|
||||
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)
|
||||
continue
|
||||
|
||||
_waiting_logged = False # Reset so we log again if user unregisters
|
||||
await run_sync(settings=current_settings)
|
||||
except Exception as 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
|
||||
log(f"Sleeping for {interval} seconds...")
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
@@ -6,3 +6,4 @@ python-telegram-bot==21.6
|
||||
qrcode==8.0
|
||||
requests==2.32.3
|
||||
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
|
||||
:: ==============================================================================
|
||||
:: miband-bot Smart Setup Script for Windows (100% Flat robust version)
|
||||
:: ==============================================================================
|
||||
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
|
||||
|
||||
echo ======================================================================
|
||||
echo 🚀 УМНЫЙ ЗАПУСК И НАСТРОЙКА miband-bot 🚀
|
||||
echo ======================================================================
|
||||
echo Этот скрипт поможет вам быстро развернуть личного Telegram-бота
|
||||
echo для сбора данных Xiaomi Fitness / Mi Band.
|
||||
echo.
|
||||
|
||||
:: ------------------------------------------------------------------------------
|
||||
:: Шаг 1. Выбор метода установки
|
||||
:: ------------------------------------------------------------------------------
|
||||
echo Как вы хотите запустить бота?
|
||||
echo 1] Напрямую на Python (Рекомендуется. Потребляет минимум памяти)
|
||||
echo 2] В Docker контейнерах (Всё работает в фоне)
|
||||
echo miband-bot
|
||||
echo.
|
||||
|
||||
:CHOOSE_MODE
|
||||
if "%AUTO_START%"=="1" if exist secrets.env goto MODE_PYTHON
|
||||
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%"=="2" goto MODE_DOCKER
|
||||
echo Неверный ввод. Пожалуйста, введите 1 или 2.
|
||||
echo.
|
||||
goto CHOOSE_MODE
|
||||
|
||||
:: ------------------------------------------------------------------------------
|
||||
:: Шаг 2. Проверка требований
|
||||
:: ------------------------------------------------------------------------------
|
||||
:MODE_DOCKER
|
||||
echo.
|
||||
echo [1/3] Проверка окружения Docker...
|
||||
where docker >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo ❌ Docker CLI не найден. Пожалуйста, установите Docker Desktop или настройте WSL 2.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
if errorlevel 1 ( echo ! Docker не найден. Установите Docker Desktop. & pause & exit /b 1 )
|
||||
docker info >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo ❌ Демон Docker не запущен. Пожалуйста, запустите Docker и перезапустите скрипт.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo ✓ Движок Docker активен и готов к работе!
|
||||
if errorlevel 1 ( echo ! Docker не запущен. Запустите Docker Desktop. & pause & exit /b 1 )
|
||||
echo OK Docker готов
|
||||
set DOCKER_ACTIVE=1
|
||||
goto SETUP_ENV
|
||||
|
||||
:MODE_PYTHON
|
||||
echo.
|
||||
echo [1/3] Проверка окружения Python...
|
||||
python --version >nul 2>nul
|
||||
if not errorlevel 1 goto PYTHON_OK
|
||||
|
||||
echo ❌ Python не найден или не настроен в вашей системе.
|
||||
echo Вы можете скачать его вручную по ссылке: https://www.python.org/downloads/
|
||||
echo ! Python не найден: https://www.python.org/downloads/
|
||||
echo.
|
||||
set install_python=y
|
||||
set /p install_python="Хотите, чтобы я автоматически скачал и установил Python 3.11? [Y/n]: "
|
||||
if /i not "%install_python%"=="y" (
|
||||
echo Установка отменена. Пожалуйста, установите Python вручную.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
set /p install_python=" Установить Python 3.11 автоматически? [Y/n]: "
|
||||
if /i not "%install_python%"=="y" ( echo Установите Python вручную и повторите. & pause & exit /b 1 )
|
||||
|
||||
echo.
|
||||
echo Скачивание установщика Python 3.11.9...
|
||||
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 Установка Python (это займет около минуты, пожалуйста, подождите)...
|
||||
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'"
|
||||
echo Установка...
|
||||
python_installer.exe /quiet InstallAllUsers=0 PrependPath=1 Include_test=0
|
||||
del python_installer.exe
|
||||
|
||||
:: Добавляем установленный Python в PATH текущей сессии
|
||||
set "PATH=%LocalAppData%\Programs\Python\Python311;%LocalAppData%\Programs\Python\Python311\Scripts;%PATH%"
|
||||
|
||||
python --version >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo ❌ Не удалось автоматически установить Python или добавить его в PATH.
|
||||
echo Пожалуйста, установите Python вручную по ссылке: https://www.python.org/downloads/
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
if errorlevel 1 ( echo ! Не удалось установить Python. & pause & exit /b 1 )
|
||||
|
||||
:PYTHON_OK
|
||||
echo ✓ Python найден!
|
||||
echo OK Python готов
|
||||
set DOCKER_ACTIVE=0
|
||||
goto SETUP_ENV
|
||||
|
||||
:: ------------------------------------------------------------------------------
|
||||
:: Шаг 3. Настройка secrets.env
|
||||
:: ------------------------------------------------------------------------------
|
||||
:SETUP_ENV
|
||||
echo.
|
||||
echo [2/3] Конфигурация параметров бота (secrets.env)...
|
||||
|
||||
if not exist secrets.env goto INPUT_ENV_VALUES
|
||||
echo Файл конфигурации secrets.env уже существует.
|
||||
if "%AUTO_START%"=="1" goto LAUNCH_PHASE
|
||||
set overwrite_env=n
|
||||
set /p overwrite_env="Хотите перезаписать его и настроить заново? [y/N]: "
|
||||
if /i not "%overwrite_env%"=="y" (
|
||||
echo ✓ Сохраняем существующий secrets.env.
|
||||
goto LAUNCH_PHASE
|
||||
)
|
||||
set /p overwrite_env=" Конфигурация уже есть. Перенастроить? [y/N]: "
|
||||
if /i not "%overwrite_env%"=="y" goto LAUNCH_PHASE
|
||||
|
||||
:INPUT_ENV_VALUES
|
||||
echo.
|
||||
echo Сейчас мы настроим основные параметры безопасности.
|
||||
echo.
|
||||
echo Токен бота от @BotFather в Telegram:
|
||||
|
||||
:INPUT_TOKEN
|
||||
echo 🔑 Шаг 1. Получение Telegram Bot Token
|
||||
echo Для работы бота необходим токен. Его можно получить бесплатно за 1 минуту:
|
||||
echo 1. Откройте Telegram и перейдите к официальному боту @BotFather.
|
||||
echo 2. Отправьте ему команду /newbot.
|
||||
echo 3. Введите название для бота (например, Мой Mi Band Бот).
|
||||
echo 4. Введите уникальное имя пользователя (username) на английском,
|
||||
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.
|
||||
set bot_token=
|
||||
set /p bot_token=" Токен: "
|
||||
if "%bot_token%"=="" goto TOKEN_EMPTY
|
||||
echo %bot_token% | find ":" >nul 2>nul
|
||||
if errorlevel 1 goto TOKEN_INVALID
|
||||
goto TOKEN_OK
|
||||
|
||||
echo 🔒 Шаг 2. Автоматическая привязка владельца (Whitelist ID)
|
||||
echo Вам НЕ НУЖНО вручную искать и вводить ваш Telegram User ID!
|
||||
echo Сразу после запуска бота откройте его в Telegram и отправьте команду /start.
|
||||
echo Бот автоматически распознает ваш ID, запишет его в белый список
|
||||
echo и заблокирует доступ для всех остальных пользователей.
|
||||
echo.
|
||||
:TOKEN_EMPTY
|
||||
echo Токен не может быть пустым.
|
||||
goto INPUT_TOKEN
|
||||
|
||||
:: Создание secrets.env (запись построчно во избежание багов парсера скобок)
|
||||
echo # ============================================================================== > secrets.env
|
||||
echo # Конфигурация secrets.env для miband-bot >> secrets.env
|
||||
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
|
||||
:TOKEN_INVALID
|
||||
echo Неверный формат - токен должен содержать двоеточие.
|
||||
goto INPUT_TOKEN
|
||||
|
||||
echo ✓ Файл secrets.env успешно создан!
|
||||
echo.
|
||||
:TOKEN_OK
|
||||
(
|
||||
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
|
||||
echo [3/3] Подготовка среды выполнения...
|
||||
|
||||
echo.
|
||||
if not "%DOCKER_ACTIVE%"=="1" goto PYTHON_LAUNCH
|
||||
|
||||
:DOCKER_LAUNCH
|
||||
set launch_now=y
|
||||
set /p launch_now="Хотите запустить miband-bot в Docker прямо сейчас? [Y/n]: "
|
||||
if /i not "%launch_now%"=="y" (
|
||||
echo.
|
||||
echo Бот настроен, но не запущен.
|
||||
echo Запустите вручную командой: docker compose up -d --build
|
||||
goto END_LAUNCH
|
||||
)
|
||||
echo Сборка и запуск контейнеров...
|
||||
set /p launch_now=" Запустить в Docker сейчас? [Y/n]: "
|
||||
if /i not "%launch_now%"=="y" ( echo Запуск вручную: docker compose up -d --build & goto END_LAUNCH )
|
||||
docker compose up -d --build
|
||||
if not errorlevel 1 (
|
||||
echo.
|
||||
echo ======================================================================
|
||||
echo 🎉 miband-bot УСПЕШНО НАСТРОЕН И ЗАПУЩЕН В DOCKER! 🎉
|
||||
echo ======================================================================
|
||||
echo Теперь откройте бота в Telegram и отправьте команду /start.
|
||||
echo.
|
||||
echo Логи: docker compose logs -f fitness-bot
|
||||
echo Стоп: docker compose down
|
||||
echo ======================================================================
|
||||
echo OK Бот запущен в Docker. Отправьте /start в Telegram.
|
||||
echo Логи: docker compose logs -f fitness-bot
|
||||
) else (
|
||||
echo Ошибка при сборке или запуске Docker Compose.
|
||||
echo ! Ошибка запуска Docker.
|
||||
)
|
||||
goto END_LAUNCH
|
||||
|
||||
:PYTHON_LAUNCH
|
||||
echo Создаем виртуальное окружение .venv и устанавливаем зависимости...
|
||||
if not exist .venv (
|
||||
python -m venv .venv
|
||||
)
|
||||
echo Подготовка...
|
||||
if not exist .venv ( python -m venv .venv >nul 2>nul )
|
||||
call .venv\Scripts\activate
|
||||
echo Установка библиотек...
|
||||
pip install -r requirements.txt -e mi-fitness-python
|
||||
pip install -r requirements.txt -e mi-fitness-python > pip_install.log 2>&1
|
||||
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 построчно (полностью без скобочных блоков)
|
||||
echo @echo off > run_local.bat
|
||||
echo chcp 65001 ^> nul >> run_local.bat
|
||||
echo cd /d "%%~dp0" >> run_local.bat
|
||||
echo if not exist .venv ^( >> run_local.bat
|
||||
echo echo Ошибка: Виртуальное окружение .venv не найдено. Запустите setup.bat сначала. >> run_local.bat
|
||||
echo pause >> run_local.bat
|
||||
echo exit /b 1 >> run_local.bat
|
||||
echo ^) >> run_local.bat
|
||||
echo. >> run_local.bat
|
||||
echo :: Загрузка переменных из secrets.env >> run_local.bat
|
||||
echo if exist secrets.env ^( >> run_local.bat
|
||||
echo for /f "usebackq delims=" %%%%x in ^("secrets.env"^) do ^( >> run_local.bat
|
||||
echo echo %%%%x ^| findstr /r "^#" ^>nul >> run_local.bat
|
||||
echo if errorlevel 1 ^( >> run_local.bat
|
||||
echo set %%%%x >> run_local.bat
|
||||
echo ^) >> run_local.bat
|
||||
echo ^) >> run_local.bat
|
||||
echo ^) >> run_local.bat
|
||||
echo. >> run_local.bat
|
||||
echo :: Настройка локальных путей к папке data >> run_local.bat
|
||||
echo set DATA_DIR=.\data >> run_local.bat
|
||||
echo set DB_PATH=.\data\miband_%%TELEGRAM_ALLOWED_USER_ID%%.db >> run_local.bat
|
||||
echo set STATUS_PATH=.\data\status_%%TELEGRAM_ALLOWED_USER_ID%%.json >> run_local.bat
|
||||
echo set BOT_STATE_DB_PATH=.\data\fitness_bot_state.db >> run_local.bat
|
||||
echo. >> run_local.bat
|
||||
echo call .venv\Scripts\activate >> run_local.bat
|
||||
echo === Запуск miband-bot локально ^(без Docker^) === >> run_local.bat
|
||||
echo Для завершения работы закройте это окно консоли. >> run_local.bat
|
||||
echo. >> run_local.bat
|
||||
echo Запуск синхронизатора и Telegram-бота... >> run_local.bat
|
||||
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
|
||||
>run_local.bat echo @echo off
|
||||
>>run_local.bat echo chcp 65001 ^> nul
|
||||
>>run_local.bat echo cd /d "%%~dp0"
|
||||
>>run_local.bat echo if not exist .venv ^(
|
||||
>>run_local.bat echo echo ! .venv не найдено. Запустите setup.bat заново.
|
||||
>>run_local.bat echo pause
|
||||
>>run_local.bat echo exit /b 1
|
||||
>>run_local.bat echo ^)
|
||||
>>run_local.bat echo if not exist run.py ^(
|
||||
>>run_local.bat echo echo ! run.py не найден. Обновите проект и запустите setup.bat заново.
|
||||
>>run_local.bat echo pause
|
||||
>>run_local.bat echo exit /b 1
|
||||
>>run_local.bat echo ^)
|
||||
>>run_local.bat echo call .venv\Scripts\activate
|
||||
>>run_local.bat echo set PYTHONUTF8=1
|
||||
>>run_local.bat echo set PYTHONIOENCODING=utf-8
|
||||
>>run_local.bat echo md data 2^>nul
|
||||
>>run_local.bat echo cls
|
||||
>>run_local.bat echo echo.
|
||||
>>run_local.bat echo echo miband-bot
|
||||
>>run_local.bat echo echo.
|
||||
>>run_local.bat echo echo OK Бот запущен. Не закрывайте это окно.
|
||||
>>run_local.bat echo echo Логи: data\bot.log / data\sync.log
|
||||
>>run_local.bat echo echo.
|
||||
>>run_local.bat echo python -u run.py
|
||||
>>run_local.bat echo echo.
|
||||
>>run_local.bat echo echo Бот остановлен. Можно закрыть окно.
|
||||
>>run_local.bat echo echo.
|
||||
>>run_local.bat echo pause
|
||||
|
||||
if "%AUTO_START%"=="1" goto RUN_LOCAL_NOW
|
||||
|
||||
echo ✓ Создан удобный скрипт запуска: run_local.bat
|
||||
echo.
|
||||
set launch_now=y
|
||||
set /p launch_now="Хотите запустить бота локально прямо сейчас? [Y/n]: "
|
||||
if /i "%launch_now%"=="y" (
|
||||
call run_local.bat
|
||||
)
|
||||
set /p launch_now=" Запустить бота сейчас? [Y/n]: "
|
||||
if /i "%launch_now%"=="y" ( call run_local.bat )
|
||||
goto END_LAUNCH
|
||||
|
||||
:RUN_LOCAL_NOW
|
||||
call run_local.bat
|
||||
|
||||
:END_LAUNCH
|
||||
echo.
|
||||
|
||||
@@ -1,319 +1,171 @@
|
||||
#!/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'
|
||||
# Насыщенный желтый
|
||||
YELLOW='\033[1;33m'
|
||||
# Глубокий синий
|
||||
BLUE='\033[0;34m'
|
||||
# Пурпурный
|
||||
MAGENTA='\033[0;35m'
|
||||
# Серый / обычный
|
||||
NC='\033[0;37m'
|
||||
BOLD='\033[1m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
||||
err() { echo -e " ${RED}❌${NC} $1"; }
|
||||
|
||||
clear
|
||||
echo -e "${BLUE}${BOLD}======================================================================"
|
||||
echo -e " 🚀 УМНЫЙ ЗАПУСК И НАСТРОЙКА 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 " miband-bot"
|
||||
echo ""
|
||||
|
||||
# --- Выбор режима ---
|
||||
while true; do
|
||||
read -p "Выберите вариант (1 или 2) [1]: " install_mode
|
||||
if [ -z "$install_mode" ]; then
|
||||
install_mode="1"
|
||||
fi
|
||||
if [ "$install_mode" = "1" ] || [ "$install_mode" = "2" ]; then
|
||||
break
|
||||
else
|
||||
echo -e "${RED}Неверный ввод. Пожалуйста, введите 1 или 2.${NC}"
|
||||
fi
|
||||
read -p " [1] Python (рек.) [2] Docker Выбор [1]: " install_mode
|
||||
install_mode="${install_mode:-1}"
|
||||
[ "$install_mode" = "1" ] || [ "$install_mode" = "2" ] && break
|
||||
done
|
||||
echo ""
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Шаг 2. Проверка требований выбранного режима
|
||||
# ------------------------------------------------------------------------------
|
||||
# --- Проверка требований ---
|
||||
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
|
||||
DOCKER_AVAILABLE=false
|
||||
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."
|
||||
err "Docker не найден. Установите Docker Desktop или Colima (brew install colima)."
|
||||
exit 1
|
||||
elif [ "$DOCKER_RUNNING" = false ]; then
|
||||
echo -e "${RED}❌ Служба Docker установлена, но сейчас не запущена.${NC}"
|
||||
echo -e "Пожалуйста, запустите Docker-демон (например, 'colima start' на Mac"
|
||||
echo -e "или 'sudo systemctl start docker' на Linux) и перезапустите скрипт."
|
||||
fi
|
||||
if ! docker info &> /dev/null; then
|
||||
err "Docker не запущен. Запустите демон (colima start / Docker Desktop) и повторите."
|
||||
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
|
||||
echo -e "${GREEN}✓ Движок Docker активен и готов к работе!${NC}"
|
||||
if docker compose version &> /dev/null; then
|
||||
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
|
||||
err "Плагин docker compose не найден."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ok "Docker готов"
|
||||
DOCKER_MODE=true
|
||||
else
|
||||
echo -e "${BOLD}[1/3] Проверка окружения Python...${NC}"
|
||||
|
||||
PYTHON_CMD=""
|
||||
if command -v python3 &> /dev/null; then
|
||||
PYTHON_CMD="python3"
|
||||
elif command -v python &> /dev/null; then
|
||||
# Проверяем версию python
|
||||
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"
|
||||
for cmd in python3 python; do
|
||||
if command -v "$cmd" &> /dev/null; then
|
||||
ver=$("$cmd" -c 'import sys; print(sys.version_info[0])' 2>/dev/null)
|
||||
[ "$ver" = "3" ] && PYTHON_CMD="$cmd" && break
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
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}"
|
||||
if [ -z "$PYTHON_CMD" ]; then
|
||||
err "Python 3 не найден. Установите: https://www.python.org/downloads/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Создаем удобный скрипт для локального запуска
|
||||
LAUNCHER_FILE="run_local.sh"
|
||||
cat << 'EOF' > "$LAUNCHER_FILE"
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Скрипт локального запуска miband-bot (без Docker)
|
||||
# ==============================================================================
|
||||
cd "$(dirname "$0")"
|
||||
ok "Python готов ($PYTHON_CMD)"
|
||||
DOCKER_MODE=false
|
||||
fi
|
||||
|
||||
# --- Конфигурация ---
|
||||
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
|
||||
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
|
||||
fi
|
||||
|
||||
# Загружаем переменные из secrets.env в окружение текущего процесса
|
||||
if [ -f secrets.env ]; then
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
# Очищаем пробелы и переносы
|
||||
line=$(echo "$line" | xargs 2>/dev/null || echo "$line")
|
||||
# Игнорируем комментарии и пустые строки
|
||||
if [[ ! "$line" =~ ^# ]] && [[ ! -z "$line" ]]; then
|
||||
export "$line"
|
||||
fi
|
||||
done < secrets.env
|
||||
# Генерируем run_local.sh — логи Python уходят в файлы
|
||||
cat << 'EOF' > run_local.sh
|
||||
#!/usr/bin/env bash
|
||||
# Запуск miband-bot (Python читает secrets.env автоматически)
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ ! -d ".venv" ]; then
|
||||
echo " ❌ .venv не найдена. Запустите setup.sh заново."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Настраиваем локальные пути к папке data (чтобы не писать в глобальные пути /opt)
|
||||
export DATA_DIR="./data"
|
||||
export DB_PATH="./data/miband_${TELEGRAM_ALLOWED_USER_ID}.db"
|
||||
export STATUS_PATH="./data/status_${TELEGRAM_ALLOWED_USER_ID}.json"
|
||||
export BOT_STATE_DB_PATH="./data/fitness_bot_state.db"
|
||||
if [ ! -f "run.py" ]; then
|
||||
echo " ❌ run.py не найден. Обновите проект и запустите setup.sh заново."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source .venv/bin/activate
|
||||
echo "=== Запуск miband-bot локально на Python ==="
|
||||
echo "Нажмите Ctrl+C для завершения работы обоих процессов."
|
||||
mkdir -p data
|
||||
clear
|
||||
echo ""
|
||||
echo " miband-bot"
|
||||
echo ""
|
||||
echo " ✓ Бот запущен. Не закрывайте это окно."
|
||||
echo " Логи: data/bot.log / data/sync.log"
|
||||
echo ""
|
||||
|
||||
# При завершении скрипта (Ctrl+C) убиваем все запущенные нами фоновые задачи
|
||||
trap "kill 0" EXIT
|
||||
|
||||
# Запускаем фоновый синхронизатор и сам Telegram бот параллельно
|
||||
python -u miband_sync.py &
|
||||
python -u fitness_bot.py &
|
||||
|
||||
# Ожидаем завершения
|
||||
wait
|
||||
python -u run.py
|
||||
echo ""
|
||||
echo " Бот остановлен. Можно закрыть окно."
|
||||
echo ""
|
||||
EOF
|
||||
chmod +x "$LAUNCHER_FILE"
|
||||
echo -e "${GREEN}✓ Создан удобный скрипт запуска: ${BOLD}./run_local.sh${NC}"
|
||||
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
|
||||
|
||||
chmod +x run_local.sh
|
||||
ok "Создан скрипт запуска: ./run_local.sh"
|
||||
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