diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..be87b21 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore index e4fab16..64f409c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ status*.json *.zip *.tar *.tar.gz + +# Generated launchers +run_local.sh +run_local.bat diff --git a/README.md b/README.md index 7a42fbe..802e765 100644 --- a/README.md +++ b/README.md @@ -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_.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_.json` - Xiaomi auth token, секретный файл; -- `miband_.db` - SQLite-база с health-данными; -- `status_.json` - последний статус синхронизации; -- `fitness_bot_state.db` - служебное состояние Telegram-меню; -- `sync_.lock` - lock-файл синхронизации. +| Файл | Содержимое | +| ---------------------- | --------------------------------- | +| `token_.json` | Xiaomi auth token (**секретный**) | +| `miband_.db` | SQLite-база с health-данными | +| `status_.json` | Последний статус синхронизации | +| `allowed_user.id` | ID привязанного владельца | +| `fitness_bot_state.db` | Служебное состояние Telegram-меню | +| `sync_.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). diff --git a/README_EN.md b/README_EN.md index deddd62..194d8e0 100644 --- a/README_EN.md +++ b/README_EN.md @@ -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_.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_.json` - Xiaomi auth token, secret file; -- `miband_.db` - SQLite database with health data; -- `status_.json` - latest sync status; -- `fitness_bot_state.db` - Telegram menu state; -- `sync_.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_.json` | Xiaomi auth token (**secret**) | +| `miband_.db` | SQLite database with health data | +| `status_.json` | Last sync status | +| `allowed_user.id` | ID of the bound owner | +| `fitness_bot_state.db` | Telegram menu internal state | +| `sync_.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). diff --git a/fitness_bot.py b/fitness_bot.py index f56e0f0..26e4391 100644 --- a/fitness_bot.py +++ b/fitness_bot.py @@ -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() diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..e58fafc --- /dev/null +++ b/install.ps1 @@ -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 diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..11e2c67 --- /dev/null +++ b/install.sh @@ -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/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 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() diff --git a/miband_tracker/config.py b/miband_tracker/config.py index c695a98..6b62afd 100644 --- a/miband_tracker/config.py +++ b/miband_tracker/config.py @@ -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 ) diff --git a/miband_tracker/lock.py b/miband_tracker/lock.py index a8075c5..a43f20b 100644 --- a/miband_tracker/lock.py +++ b/miband_tracker/lock.py @@ -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: # Резервный вариант, если блокировки недоступны на платформе diff --git a/miband_tracker/stdio.py b/miband_tracker/stdio.py new file mode 100644 index 0000000..4432843 --- /dev/null +++ b/miband_tracker/stdio.py @@ -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) diff --git a/miband_tracker/sync.py b/miband_tracker/sync.py index a94b2e9..b70bbd0 100644 --- a/miband_tracker/sync.py +++ b/miband_tracker/sync.py @@ -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) diff --git a/requirements.txt b/requirements.txt index 1ec4c07..78f57e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ python-telegram-bot==21.6 qrcode==8.0 requests==2.32.3 tenacity==9.1.2 +tzdata diff --git a/run.py b/run.py new file mode 100644 index 0000000..aaefc21 --- /dev/null +++ b/run.py @@ -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() diff --git a/setup.bat b/setup.bat index dc7cb1d..bc43659 100644 --- a/setup.bat +++ b/setup.bat @@ -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. diff --git a/setup.sh b/setup.sh index 6914b4d..395ac76 100755 --- a/setup.sh +++ b/setup.sh @@ -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 diff --git a/tests/test_stdio.py b/tests/test_stdio.py new file mode 100644 index 0000000..0fcdeec --- /dev/null +++ b/tests/test_stdio.py @@ -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()